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.
- data/.gitignore +27 -0
- data/.idea/.name +1 -0
- data/.idea/compiler.xml +23 -0
- data/.idea/copyright/profiles_settings.xml +3 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/inspectionProfiles/Project_Default.xml +11 -0
- data/.idea/inspectionProfiles/profiles_settings.xml +7 -0
- data/.idea/misc.xml +23 -0
- data/.idea/modules.xml +9 -0
- data/.idea/scopes/scope_settings.xml +5 -0
- data/.idea/vcs.xml +7 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/LICENSE.md +22 -0
- data/README.md +227 -0
- data/Rakefile +73 -0
- data/bill_forward.gemspec +29 -0
- data/bill_forward.iml +28 -0
- data/lib/bill_forward.rb +18 -0
- data/lib/bill_forward/billing_entity.rb +263 -0
- data/lib/bill_forward/client.rb +355 -0
- data/lib/bill_forward/custom_hash.rb +14 -0
- data/lib/bill_forward/deny_method.rb +4 -0
- data/lib/bill_forward/entities/account.rb +19 -0
- data/lib/bill_forward/entities/address.rb +25 -0
- data/lib/bill_forward/entities/amendments/amendment.rb +11 -0
- data/lib/bill_forward/entities/amendments/invoice_recalculation_amendment.rb +10 -0
- data/lib/bill_forward/entities/api_configuration.rb +11 -0
- data/lib/bill_forward/entities/authorize_net_token.rb +9 -0
- data/lib/bill_forward/entities/credit_note.rb +13 -0
- data/lib/bill_forward/entities/invoice.rb +25 -0
- data/lib/bill_forward/entities/invoice_parts/invoice_line.rb +37 -0
- data/lib/bill_forward/entities/invoice_parts/invoice_payment.rb +29 -0
- data/lib/bill_forward/entities/invoice_parts/tax_line.rb +23 -0
- data/lib/bill_forward/entities/invoice_parts/taxation_link.rb +5 -0
- data/lib/bill_forward/entities/organisation.rb +37 -0
- data/lib/bill_forward/entities/payment_method.rb +5 -0
- data/lib/bill_forward/entities/payment_method_subscription_link.rb +5 -0
- data/lib/bill_forward/entities/pricing_component.rb +21 -0
- data/lib/bill_forward/entities/pricing_component_tier.rb +5 -0
- data/lib/bill_forward/entities/pricing_component_value.rb +5 -0
- data/lib/bill_forward/entities/pricing_component_value_change.rb +5 -0
- data/lib/bill_forward/entities/product.rb +5 -0
- data/lib/bill_forward/entities/product_rate_plan.rb +19 -0
- data/lib/bill_forward/entities/profile.rb +15 -0
- data/lib/bill_forward/entities/role.rb +4 -0
- data/lib/bill_forward/entities/subscription.rb +53 -0
- data/lib/bill_forward/entities/unit_of_measure.rb +5 -0
- data/lib/bill_forward/insertable_entity.rb +32 -0
- data/lib/bill_forward/mutable_entity.rb +47 -0
- data/lib/bill_forward/resource_path.rb +11 -0
- data/lib/bill_forward/type_check.rb +21 -0
- data/lib/bill_forward/version.rb +4 -0
- data/spec/component/account_spec.rb +200 -0
- data/spec/component/billing_entity_spec.rb +153 -0
- data/spec/component/invoice_spec.rb +155 -0
- data/spec/component/subscription_spec.rb +357 -0
- data/spec/functional/account_spec.rb +25 -0
- data/spec/functional/bad_citizen/account_spec.rb +103 -0
- data/spec/functional/bad_citizen/credit_note_spec.rb +41 -0
- data/spec/functional/bad_citizen/payment_method_spec.rb +34 -0
- data/spec/functional/bad_citizen/product_rate_plan_spec.rb +105 -0
- data/spec/functional/bad_citizen/product_spec.rb +22 -0
- data/spec/functional/bad_citizen/situational/authorize_net_token_spec.rb +27 -0
- data/spec/functional/bad_citizen/situational/invoice_recalculation_amendment_spec.rb +27 -0
- data/spec/functional/bad_citizen/situational/invoice_spec.rb +22 -0
- data/spec/functional/bad_citizen/situational/malordered_entity_spec.rb +43 -0
- data/spec/functional/bad_citizen/situational/organisation_spec.rb +39 -0
- data/spec/functional/bad_citizen/situational/payment_method_spec.rb +47 -0
- data/spec/functional/bad_citizen/situational/subscription_chargeable_spec.rb +255 -0
- data/spec/functional/bad_citizen/subscription_spec.rb +179 -0
- data/spec/functional/bad_citizen/subscription_with_credit_spec.rb +240 -0
- data/spec/functional/bad_citizen/unit_of_measure_spec.rb +20 -0
- data/spec/functional/billing_entity_spec.rb +22 -0
- data/spec/functional/client_spec.rb +24 -0
- data/spec/functional/organisation_spec.rb +28 -0
- data/spec/setup_test_constants.rb +73 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/syntax/account_spec.rb +24 -0
- data/spec/syntax/address_spec.rb +19 -0
- data/spec/syntax/api_configuration_spec.rb +13 -0
- data/spec/syntax/billing_entity_spec.rb +93 -0
- data/spec/syntax/client_spec.rb +8 -0
- 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
|
+
|
data/lib/bill_forward.rb
ADDED
|
@@ -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
|