cheddargetter_client 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://rubygems.org'
2
+ gem 'httparty'
3
+ gem "crack"
4
+
5
+ group :development do
6
+ gem 'rspec'
7
+ gem "bundler"
8
+ gem "jeweler"
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ crack (0.3.2)
5
+ diff-lcs (1.1.3)
6
+ git (1.2.5)
7
+ httparty (0.10.0)
8
+ multi_json (~> 1.0)
9
+ multi_xml
10
+ jeweler (1.8.4)
11
+ bundler (~> 1.0)
12
+ git (>= 1.2.5)
13
+ rake
14
+ rdoc
15
+ json (1.7.6)
16
+ multi_json (1.5.0)
17
+ multi_xml (0.5.2)
18
+ rake (10.0.3)
19
+ rdoc (3.12)
20
+ json (~> 1.4)
21
+ rspec (2.12.0)
22
+ rspec-core (~> 2.12.0)
23
+ rspec-expectations (~> 2.12.0)
24
+ rspec-mocks (~> 2.12.0)
25
+ rspec-core (2.12.2)
26
+ rspec-expectations (2.12.1)
27
+ diff-lcs (~> 1.1.3)
28
+ rspec-mocks (2.12.1)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler
35
+ crack
36
+ httparty
37
+ jeweler
38
+ rspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Brent Wooden
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ This project borrows highly from cheddargetter_client_ruby.
2
+ I completely reuse their response object.
3
+
4
+ I felt however their implementation of calls to the Cheddargetter api were too static,
5
+ so I implemented a wrapper using method missing. This way if methods
6
+ are added to the api, they can be easily accessed. Here are some mappings.
7
+ There should be a noticable pattern.
8
+
9
+
10
+ - #/customers/get
11
+ -- cg_client_object.customers_get({:customer_code => 'ABCD'}) (code optional)
12
+ - #/customers/new
13
+ -- cg_client_object.customers_new({
14
+ :customer_code => CustomerCode,
15
+ :firstName => 'W Pain',
16
+ :lastName => 'Alemeda',
17
+ :email => 'w_pain@example.com',
18
+ :subscription => {:planCode => PlanCodes.last}
19
+ })
20
+
21
+ - #/customers/edit
22
+ -- cg_client.customers_edit({:customer_code => 'ABCD'}, *valid_customer_attributes_hash*)
23
+ - #/customers/edit-customer
24
+ -- cg_client.customers_edit_customer({:customer_code => 'ABCD'}, *valid_customer_attributes_hash*)
25
+ - #/customers/edit-subscription
26
+ -- cg_client.customers_edit_subscription({:customer_code => 'ABCD'}, {:subscription => *ValidSubscription*})
27
+ - #/customers/delete
28
+ -- cg_client.customers_delete({:customer_code => 'ABCD'})
29
+ - #/customers/cancel
30
+ -- cg_client.customers_cancel({:customer_code => 'ABCD'})
31
+ - #/customers/add-item-quantity
32
+ -- cg_client.customers_add_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}
33
+ - #/customers/remove-item-quantity
34
+ -- cg_client.customers_remove_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}, {:quantity => 5})
35
+ - #/customers/set-item-quantity
36
+ cg_client.customers_set_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}, {:quantity => 13}
37
+
38
+ See the specs more examples. These are copied from them.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "cheddargetter_client"
18
+ gem.homepage = "http://github.com/BrentW/cheddargetter_client"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A ruby wrapper for the Cheddargetter API}
21
+ gem.description = %Q{A more flexible solution for accessing the Cheddargetter API}
22
+ gem.email = "brent.wooden@gmail.com"
23
+ gem.authors = ["Brent Wooden"]
24
+ # dependencies defined in Gemfile
25
+
26
+ gem.add_dependency "httparty", ">= 0.10.0"
27
+ gem.add_dependency "crack", ">= 0.3.2"
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "cheddargetter_client"
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brent Wooden"]
12
+ s.date = "2013-01-25"
13
+ s.description = "A more flexible solution for accessing the Cheddargetter API"
14
+ s.email = "brent.wooden@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "README.md",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "cheddargetter_client.gemspec",
27
+ "lib/cheddargetter/client.rb",
28
+ "lib/cheddargetter/response.rb",
29
+ "lib/cheddargetter_client.rb",
30
+ "pkg/cheddargetter_client-0.0.0.gem",
31
+ "spec/cheddar_getter_client_spec.rb",
32
+ "spec/spec_helper.rb"
33
+ ]
34
+ s.homepage = "http://github.com/BrentW/cheddargetter_client"
35
+ s.licenses = ["MIT"]
36
+ s.require_paths = ["lib"]
37
+ s.rubygems_version = "1.8.24"
38
+ s.summary = "A ruby wrapper for the Cheddargetter API"
39
+
40
+ if s.respond_to? :specification_version then
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
+ s.add_runtime_dependency(%q<httparty>, [">= 0"])
45
+ s.add_runtime_dependency(%q<crack>, [">= 0"])
46
+ s.add_development_dependency(%q<rspec>, [">= 0"])
47
+ s.add_development_dependency(%q<bundler>, [">= 0"])
48
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
49
+ s.add_runtime_dependency(%q<httparty>, [">= 0.10.0"])
50
+ s.add_runtime_dependency(%q<crack>, [">= 0.3.2"])
51
+ else
52
+ s.add_dependency(%q<httparty>, [">= 0"])
53
+ s.add_dependency(%q<crack>, [">= 0"])
54
+ s.add_dependency(%q<rspec>, [">= 0"])
55
+ s.add_dependency(%q<bundler>, [">= 0"])
56
+ s.add_dependency(%q<jeweler>, [">= 0"])
57
+ s.add_dependency(%q<httparty>, [">= 0.10.0"])
58
+ s.add_dependency(%q<crack>, [">= 0.3.2"])
59
+ end
60
+ else
61
+ s.add_dependency(%q<httparty>, [">= 0"])
62
+ s.add_dependency(%q<crack>, [">= 0"])
63
+ s.add_dependency(%q<rspec>, [">= 0"])
64
+ s.add_dependency(%q<bundler>, [">= 0"])
65
+ s.add_dependency(%q<jeweler>, [">= 0"])
66
+ s.add_dependency(%q<httparty>, [">= 0.10.0"])
67
+ s.add_dependency(%q<crack>, [">= 0.3.2"])
68
+ end
69
+ end
70
+
@@ -0,0 +1,105 @@
1
+ module Cheddargetter
2
+ class Client
3
+ include HTTParty
4
+ base_uri 'https://cheddargetter.com/xml'
5
+
6
+ attr_accessor :username, :password, :product_code, :options_mode
7
+
8
+ OptionModes = [:customer, :data, :customer_and_data, :no_options]
9
+
10
+ def initialize(options = {})
11
+ self.username = options[:username] || options["username"]
12
+ self.password = options[:password] || options["password"]
13
+ self.product_code = options[:product_code] || options["product_code"]
14
+
15
+ raise(ArgumentError, "No username specified") unless username
16
+ raise(ArgumentError, "No password specified") unless password
17
+ raise(ArgumentError, "No product_code specified") unless product_code
18
+ end
19
+
20
+ def method_missing(method, *args, &block)
21
+ parts = method.to_s.split('_')
22
+ response = post('/' + [parts.slice!(0, 2).join('/'), parts].flatten.join('-'), args)
23
+
24
+ if response.parsed_response["error"] && response.parsed_response["error"]["__content__"] && response.parsed_response["error"]["__content__"] == 'Resource not found'
25
+ raise NoMethodError.new(method.to_s)
26
+ else
27
+ ::Cheddargetter::Response.new(response)
28
+ end
29
+ end
30
+
31
+ def post(url, options)
32
+ set_options_mode(options)
33
+
34
+ response = self.class.post(
35
+ full_url(url, options),
36
+ :body => filter_customer_code_key(options),
37
+ :basic_auth => {:username => username, :password => password}
38
+ )
39
+ end
40
+
41
+ def customers_delete_all
42
+ #whacky url
43
+ response = self.class.post(
44
+ "https://cheddargetter.com/xml/customers/delete-all/confirm/#{Time.now.to_i}/productCode/#{product_code}",
45
+ :basic_auth => {:username => username, :password => password}
46
+ )
47
+ ::Cheddargetter::Response.new(response)
48
+ end
49
+
50
+ private
51
+
52
+ def full_url(url, options = [])
53
+ return_url = url_with_product(url)
54
+
55
+ (options.first || {}).each do |name, value|
56
+ return_url += param_from_attribute_name(name, value) || ''
57
+ end unless options_mode == :data
58
+ return_url
59
+ end
60
+
61
+ def set_options_mode(options)
62
+ if !options || options.length == 0
63
+ self.options_mode = :no_options
64
+ elsif options.is_a?(Array) && options.size == 2
65
+ self.options_mode = :customer_and_data
66
+ elsif options.first.size == 1
67
+ self.options_mode = :customer
68
+ else
69
+ self.options_mode = :data
70
+ end
71
+ end
72
+
73
+ def filter_customer_code_key(array)
74
+ return {} if !array || array.length == 0
75
+ hash = (array && array.is_a?(Array) && array.last) || array
76
+ hash[:code] = hash.delete(:customer_code) if hash[:customer_code]
77
+ hash
78
+ end
79
+
80
+ def url_with_product(url)
81
+ url + product_param
82
+ end
83
+
84
+ def product_param
85
+ "/productCode/#{product_code}"
86
+ end
87
+
88
+ def param_from_attribute_name(name, value)
89
+ name = 'code' if name.to_sym == :customer_code
90
+ name_parts = name.to_s.split('_')
91
+ if name_parts.length > 1
92
+ name = name.to_s.split('_').map(&:capitalize).join.tap{|str| str.match(/(^\w)/)}.gsub((/^\w/), $1.downcase)
93
+ end
94
+ "/#{name}/#{value}" if value
95
+ end
96
+
97
+ def customer_param(customer_code)
98
+ "/code/#{customer_code}" if customer_code
99
+ end
100
+
101
+ def item_param(item_code)
102
+ "/itemCode/#{item_code}" if item_code
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,416 @@
1
+ module Cheddargetter
2
+ class Response
3
+ SPECIAL_ARRAY_KEYS = {
4
+ :plans => :plan,
5
+ :items => :item,
6
+ :subscriptions => :subscription,
7
+ :customers => :customer,
8
+ :invoices => :invoice,
9
+ :charges => :charge,
10
+ :transactions => :transaction,
11
+ :errors => :error
12
+ }
13
+
14
+ #not charges, because codes aren't unique
15
+ #not invoices, transactions, subscriptions, or errors because they don't have codes
16
+ ARRAY_TO_HASH_BY_CODE =
17
+ [:plans, :items, :customers]
18
+
19
+ KEY_TO_DATA_TYPE = {
20
+ :isActive => :boolean,
21
+ :isFree => :boolean,
22
+ :trialDays => :integer,
23
+ :setupChargeAmount => :float,
24
+ :recurringChargeAmount => :float,
25
+ :billingFrequencyQuantity => :integer,
26
+ :createdDatetime => :datetime,
27
+ :quantityIncluded => :float,
28
+ :isPeriodic => :boolean,
29
+ :overageAmount => :float,
30
+ :isVatExempt => :boolean,
31
+ :firstContactDatetime => :datetime,
32
+ :modifiedDatetime => :datetime,
33
+ :canceledDatetime => :datetime,
34
+ :ccExpirationDate => :date,
35
+ :quantity => :float,
36
+ :billingDatetime => :datetime,
37
+ :eachAmount => :float,
38
+ :number => :integer,
39
+ :amount => :float,
40
+ :transactedDatetime => :datetime,
41
+ :vatRate => :float
42
+ }
43
+
44
+ attr_accessor :raw_response, :clean_response
45
+
46
+ def initialize(response) #:nodoc:
47
+ self.raw_response = response
48
+ create_clean_response
49
+ end
50
+
51
+ [:errors, :plans, :customers].each do |key|
52
+ define_method key.to_s do
53
+ self[key]
54
+ end
55
+ end
56
+
57
+ #true if the response from CheddarGetter was valid
58
+ def valid?
59
+ self.errors.size == 0 && self.raw_response.code < 400
60
+ end
61
+
62
+ #the error messages (if there were any) if the CheddarGetter response was invalid
63
+ def error_messages
64
+ self.errors.map do |e|
65
+ msg = nil
66
+ if e
67
+ msg = e[:text]
68
+ msg += ": #{e[:fieldName]}" unless e[:fieldName].blank?
69
+ end
70
+ msg
71
+ end
72
+ end
73
+
74
+ #Returns the given plan.
75
+ #
76
+ #code must be provided if this response contains more than one plan.
77
+ def plan(code = nil)
78
+ retrieve_item(self, :plans, code)
79
+ end
80
+
81
+ #Returns the items for the given plan.
82
+ #
83
+ #code must be provided if this response contains more than one plan.
84
+ def plan_items(code = nil)
85
+ (plan(code) || { })[:items]
86
+ end
87
+
88
+ #Returns the given item for the given plan.
89
+ #
90
+ #item_code must be provided if the plan has more than one item.
91
+ #
92
+ #code must be provided if this response contains more than one plan.
93
+ def plan_item(item_code = nil, code = nil)
94
+ retrieve_item(plan(code), :items, item_code)
95
+ end
96
+
97
+ #Returns the given customer.
98
+ #
99
+ #code must be provided if this response contains more than one customer.
100
+ def customer(code = nil)
101
+ retrieve_item(self, :customers, code)
102
+ end
103
+
104
+ #Returns the current subscription for the given customer.
105
+ #
106
+ #code must be provided if this response contains more than one customer.
107
+ def customer_subscription(code = nil)
108
+ #current subscription is always the first one
109
+ (customer_subscriptions(code) || []).first
110
+ end
111
+
112
+ def customer_paypal_preapproval_url(code = nil)
113
+ customer_subscription[:redirectUrl] || ""
114
+ end
115
+
116
+ #Returns all the subscriptions for the given customer.
117
+ #Only the first one is active, the rest is historical.
118
+ #
119
+ #code must be provided if this response contains more than one customer.
120
+ def customer_subscriptions(code = nil)
121
+ (customer(code) || { })[:subscriptions]
122
+ end
123
+
124
+ #Returns the current plan for the given customer
125
+ #
126
+ #code must be provided if this response contains more than one customer.
127
+ def customer_plan(code = nil)
128
+ (customer_subscription(code) || { })[:plan]
129
+ end
130
+
131
+ #Returns the current open invoice for the given customer.
132
+ #
133
+ #code must be provided if this response contains more than one customer.
134
+ def customer_invoice(code = nil)
135
+ #current invoice is always the first one
136
+ ((customer_subscription(code) || { })[:invoices] || []).first
137
+ end
138
+
139
+ #Returns all of the invoices for the given customer.
140
+ #
141
+ #code must be provided if this response contains more than one customer.
142
+ def customer_invoices(code = nil)
143
+ (customer_subscriptions(code) || []).map{ |s| s[:invoices] || [] }.flatten
144
+ end
145
+
146
+ #Returns the last billed invoice for the given customer.
147
+ #This is not the same as the currect active invoice.
148
+ #
149
+ #nil if there is no last billed invoice.
150
+ #
151
+ #code must be provided if this response contains more than one customer.
152
+ def customer_last_billed_invoice(code = nil)
153
+ #last billed invoice is always the second one
154
+ (customer_invoices(code) || [])[1]
155
+ end
156
+
157
+ #Returns all of the transactions for the given customer.
158
+ #
159
+ #code must be provided if this response contains more than one customer.
160
+ def customer_transactions(code = nil)
161
+ customer_invoices(code).map{ |s| s[:transactions] || [] }.flatten
162
+ end
163
+
164
+ def customer_one_time_invoices(code = nil)
165
+ customer_invoices(code).select{ |s| s[:type] == 'one-time' }
166
+ end
167
+ #Returns the last transaction for the given customer.
168
+ #
169
+ #nil if there are no transactions.
170
+ #
171
+ #code must be provided if this response contains more than one customer.
172
+ def customer_last_transaction(code = nil)
173
+ invoice = customer_last_billed_invoice(code) || { }
174
+ (invoice[:transactions] || []).first
175
+ end
176
+
177
+ #Returns an array of any currently outstanding invoices for the given customer.
178
+ #
179
+ #code must be provided if this response contains more than one customer.
180
+ def customer_outstanding_invoices(code = nil)
181
+ now = Time.now
182
+ customer_invoices(code).reject do |i|
183
+ i[:paidTransactionId] || i[:billingDatetime] > now
184
+ end
185
+ end
186
+
187
+ #Info about the given item for the given customer.
188
+ #Merges the plan item info with the subscription item info.
189
+ #
190
+ #item_code must be provided if the customer is on a plan with more than one item.
191
+ #
192
+ #code must be provided if this response contains more than one customer.
193
+ def customer_item(item_code = nil, code = nil)
194
+ sub_item = retrieve_item(customer_subscription(code), :items, item_code)
195
+ plan_item = retrieve_item(customer_plan(code), :items, item_code)
196
+ return nil unless sub_item && plan_item
197
+ item = plan_item.dup
198
+ item[:quantity] = sub_item[:quantity]
199
+ item
200
+ end
201
+
202
+ #The amount remaining for a given item for a given customer.
203
+ #
204
+ #item_code must be provided if the customer is on a plan with more than one item.
205
+ #
206
+ #code must be provided if this response contains more than one customer.
207
+ def customer_item_quantity_remaining(item_code = nil, code = nil)
208
+ item = customer_item(item_code, code)
209
+ item ? item[:quantityIncluded] - item[:quantity] : 0
210
+ end
211
+
212
+ #The overage amount for the given item for the given customer. 0 if they are still under their limit.
213
+ #
214
+ #item_code must be provided if the customer is on a plan with more than one item.
215
+ #
216
+ #code must be provided if this response contains more than one customer.
217
+ def customer_item_quantity_overage(item_code = nil, code = nil)
218
+ over = -customer_item_quantity_remaining(item_code, code)
219
+ over = 0 if over <= 0
220
+ over
221
+ end
222
+
223
+ #The current overage cost for the given item for the given customer.
224
+ #
225
+ #item_code must be provided if the customer is on a plan with more than one item.
226
+ #
227
+ #code must be provided if this response contains more than one customer.
228
+ def customer_item_quantity_overage_cost(item_code = nil, code = nil)
229
+ item = customer_item(item_code, code)
230
+ return 0 unless item
231
+ overage = customer_item_quantity_overage(item_code, code)
232
+ item[:overageAmount] * overage
233
+ end
234
+
235
+ #true if the customer is canceled
236
+ #
237
+ #code must be provided if this reponse contains more than one customer
238
+ def customer_canceled?(code = nil)
239
+ sub = customer_subscription(code)
240
+ sub ? !!sub[:canceledDatetime] : nil
241
+ end
242
+
243
+ # Get an array representation of a single customer's current subscription
244
+ # @throws CheddarGetter_Response_Exception if the response type is incompatible or if a $code
245
+ # is not provided and the response contains more than one customer
246
+ # @return array
247
+ def customer_active?(code = nil)
248
+ subscription = customer_subscription(code)
249
+ if subscription[:canceledDatetime] && subscription[:canceledDatetime] <= Time.now
250
+ false
251
+ else
252
+ true
253
+ end
254
+ end
255
+
256
+ # Is this customer's account pending paypal preapproval confirmation?
257
+ def customer_waiting_for_paypal?(code = nil)
258
+ subscription = customer_subscription(code)
259
+ if subscription[:canceledDatetime] && subscription[:canceledDatetime] <= Time.now && subscription[:cancelType] == 'paypal-wait'
260
+ true
261
+ else
262
+ false
263
+ end
264
+ end
265
+
266
+ #access the root keys of the response directly, such as
267
+ #:customers, :plans, or :errors
268
+ def [](value)
269
+ self.clean_response[value]
270
+ end
271
+
272
+ private
273
+ def deep_fix_array_keys!(data)
274
+ if data.is_a?(Array)
275
+ data.each do |v|
276
+ deep_fix_array_keys!(v)
277
+ end
278
+ elsif data.is_a?(Hash)
279
+ data.each do |k, v|
280
+ deep_fix_array_keys!(v)
281
+ sub_key = SPECIAL_ARRAY_KEYS[k]
282
+ if sub_key && v.is_a?(Hash) && v.keys.size == 1 && v[sub_key]
283
+ data[k] = v = [v[sub_key]].flatten
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ def deep_symbolize_keys!(data)
290
+ if data.is_a?(Array)
291
+ data.each do |v|
292
+ deep_symbolize_keys!(v)
293
+ end
294
+ elsif data.is_a?(Hash)
295
+ data.keys.each do |key|
296
+ deep_symbolize_keys!(data[key])
297
+ data[(key.to_sym rescue key) || key] = data.delete(key)
298
+ end
299
+ end
300
+ end
301
+
302
+ def deep_fix_data_types!(data)
303
+ if data.is_a?(Array)
304
+ data.each do |v|
305
+ deep_fix_data_types!(v)
306
+ end
307
+ elsif data.is_a?(Hash)
308
+ data.each do |k, v|
309
+ deep_fix_data_types!(v)
310
+ type = KEY_TO_DATA_TYPE[k]
311
+ if type && v.is_a?(String)
312
+ data[k] = case type
313
+ when :integer then v.to_i
314
+ when :float then v.to_f
315
+ when :datetime then Time.parse(v) rescue v
316
+ when :date then Date.parse(v) rescue v
317
+ when :boolean then v.to_i != 0
318
+ else v
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ def make_plan_in_subscription_singular!(data)
326
+ return unless data[:customers]
327
+ data[:customers].each do |c|
328
+ c[:subscriptions].each do |s|
329
+ if s[:plans].size != 1
330
+ raise ArgumentError, "Should be exactly one plan in a subscription!"
331
+ end
332
+ s[:plan] = s[:plans].first
333
+ s.delete(:plans)
334
+ end
335
+ end
336
+ end
337
+
338
+ def switch_arrays_to_hashes!(data)
339
+ if data.is_a?(Array)
340
+ data.each do |v|
341
+ switch_arrays_to_hashes!(v)
342
+ end
343
+ elsif data.is_a?(Hash)
344
+ data.each do |k, v|
345
+ switch_arrays_to_hashes!(v)
346
+ if ARRAY_TO_HASH_BY_CODE.include?(k) && v.is_a?(Array)
347
+ new_hash = { }
348
+ v.each do |item|
349
+ new_hash[item[:code]] = item
350
+ end
351
+ data[k] = new_hash
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ def reduce_clean_errors!(data)
358
+ root_plural = [(data.delete(:errors) || { })[:error]].flatten
359
+ root_singular = [data.delete(:error)]
360
+ embed_singluar = []
361
+ embed_plural = []
362
+
363
+ embed = data[:customers] || data[:plans]
364
+ if embed
365
+ embed_singluar = [(embed.delete(:errors) || { })[:error]].flatten
366
+ embed_plural = [embed.delete(:error)]
367
+ end
368
+
369
+ new_errors = (root_plural + root_singular + embed_plural + embed_singluar).compact
370
+
371
+ new_errors.each do |e|
372
+ aux_code = (e[:auxCode] || "")
373
+ if aux_code[":"]
374
+ split_aux_code = aux_code.split(':')
375
+ e[:fieldName] = split_aux_code.first
376
+ e[:errorType] = split_aux_code.last
377
+ end
378
+ end
379
+
380
+ data[:errors] = new_errors
381
+ end
382
+
383
+ def create_clean_response
384
+ data = { }
385
+
386
+ #if it isn't a hash, then we didn't get anything useful back.
387
+ if self.raw_response.parsed_response.is_a?(Hash)
388
+ #because Crack can:t get attributes and text at the same time.
389
+ #so we fix the error blocks and re-parse
390
+ data = ::Crack::XML.parse(self.raw_response.body.gsub(/<error(.*)>(.*)<\/error>/,
391
+ '<error\1><text>\2</text></error>'))
392
+ end
393
+
394
+ deep_symbolize_keys!(data)
395
+ reduce_clean_errors!(data)
396
+ deep_fix_array_keys!(data)
397
+ deep_fix_data_types!(data)
398
+ make_plan_in_subscription_singular!(data)
399
+ switch_arrays_to_hashes!(data)
400
+ self.clean_response = data
401
+ end
402
+
403
+ def retrieve_item(root, type, code = nil)
404
+ hash = root[type]
405
+ if !hash
406
+ raise ArgumentError, "Can:t get #{type} from a response that doesn't contain #{type}"
407
+ elsif code
408
+ hash[code.to_s]
409
+ elsif hash.size <= 1
410
+ hash.first.last
411
+ else
412
+ raise ArgumentError, "This response contains multiple #{type} so you need to provide the code for the one you wish to get"
413
+ end
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,7 @@
1
+ require 'httparty'
2
+ require 'cgi' unless Object.const_defined?("CGI")
3
+
4
+ module Cheddargetter
5
+ autoload :Client, File.expand_path("lib/cheddargetter/client.rb")
6
+ autoload :Response, File.expand_path("lib/cheddargetter/response.rb")
7
+ end
Binary file
@@ -0,0 +1,285 @@
1
+ require 'spec_helper'
2
+ describe Cheddargetter::Client do
3
+ before do
4
+ class Cheddargetter::Response
5
+ def self.new(response)
6
+ response
7
+ end
8
+ end
9
+ end
10
+
11
+ Username = 'brent.wooden@gmail.com'
12
+ Password = 'j3rkface'
13
+ ProductCode = 'BEETLEBOTS'
14
+ UserCodes = ["T_ROC", "BDOG_RUEZ"]
15
+ PlanCodes = ["BEETLEBOTS", "SPIDERBOTS"]
16
+ ValidSubscription = {
17
+ :planCode => PlanCodes.first,
18
+ :ccFirstName => 'W Pain',
19
+ :ccLastName => "Almeda",
20
+ :ccNumber => '4111111111111111',
21
+ :ccExpiration => "04/#{(Date.today.year + 2).to_s}"
22
+ }
23
+
24
+ CustomerCode = "W_PAIN"
25
+ ValidCustomer = {
26
+ :customer_code => CustomerCode,
27
+ :firstName => 'W Pain',
28
+ :lastName => 'Alemeda',
29
+ :email => 'w_pain@example.com',
30
+ :subscription => {:planCode => PlanCodes.last}
31
+ }
32
+
33
+ let(:item_code) { "BATTERY" }
34
+ ChargeCode = 'LATE_FEE'
35
+
36
+ def create_customer_with_code(code)
37
+ valid_customer = {
38
+ :customer_code => code,
39
+ :firstName => code.split('_').first.downcase.capitalize,
40
+ :lastName => code.split('_').last.downcase.capitalize,
41
+ :email => "#{code.downcase}@example.com",
42
+ :subscription => {:planCode => PlanCodes.last}
43
+ }
44
+
45
+ cg_client.customers_new(valid_customer)
46
+ end
47
+
48
+
49
+ describe 'new' do
50
+ subject { Cheddargetter::Client.new(options) }
51
+ context 'with no username option' do
52
+ let(:options) {{ :password => Password, :product_code => ProductCode }}
53
+ it 'should raise correct arguement error' do
54
+ begin
55
+ subject
56
+ rescue Exception => e
57
+ e.to_s.include?("username").should be_true
58
+ end
59
+ end
60
+ end
61
+
62
+ context 'with no password option' do
63
+ let(:options) {{ :username => Username, :product_code => ProductCode }}
64
+ it 'should raise correct arguement error' do
65
+ begin
66
+ subject
67
+ rescue Exception => e
68
+ e.to_s.include?("password").should be_true
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'with no product code option' do
74
+ let(:options) {{ :password => Password, :username => Username }}
75
+ it 'should raise correct arguement error' do
76
+ begin
77
+ subject
78
+ rescue Exception => e
79
+ e.to_s.include?("product_code").should be_true
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'client actions' do
85
+ let!(:cg_client){
86
+ Cheddargetter::Client.new(
87
+ :username => 'brent.wooden@gmail.com',
88
+ :password => 'j3rkface',
89
+ :product_code => 'BEETLEBOTS'
90
+ )
91
+ }
92
+
93
+ context 'errors' do
94
+ describe 'invalid_method' do
95
+ context 'when invalid controller' do
96
+ subject { cg_client.invalid_method }
97
+ specify { lambda { subject }.should raise_error(NoMethodError) }
98
+ end
99
+
100
+ context 'when invalid action' do
101
+ subject { cg_client.customers_invalid_method }
102
+ specify { lambda { subject }.should raise_error(NoMethodError) }
103
+ end
104
+ end
105
+ end
106
+
107
+ describe 'customers_delete_all' do
108
+ subject { cg_client.customers_delete_all.to_s }
109
+ it { should include('success') }
110
+ end
111
+
112
+ context 'create/delete' do
113
+ describe 'customers_new' do
114
+ subject { cg_client.customers_new(ValidCustomer).to_s }
115
+ it { should include('Alemeda') }
116
+ end
117
+
118
+ describe 'customers_delete' do
119
+ subject { cg_client.customers_delete(:customer_code => CustomerCode).to_s }
120
+ it { should include 'success' }
121
+ end
122
+ end
123
+
124
+ context 'needing existing customers' do
125
+ before {
126
+ UserCodes.each do |code|
127
+ create_customer_with_code(code)
128
+ end
129
+ }
130
+
131
+ describe "customers_get" do
132
+ context "with no arguements get all customers" do
133
+ subject { cg_client.customers_get.to_s }
134
+
135
+ UserCodes.each do |user_code|
136
+ it { should include user_code }
137
+ end
138
+ end
139
+
140
+ context 'with specified customer' do
141
+ subject { cg_client.customers_get(:customer_code => UserCodes.first).to_s }
142
+ it { should include UserCodes.first }
143
+ it { should_not include UserCodes.last }
144
+ end
145
+ end
146
+
147
+ describe 'customers_edit_customer' do
148
+ def reset_customer
149
+ cg_client.customers_edit_customer({:customer_code => UserCodes.first},{:company => ''}).to_s
150
+ end
151
+
152
+ before { reset_customer }
153
+ subject { cg_client.customers_edit_customer({:customer_code => UserCodes.first},{:company => 'sbox'}).to_s }
154
+
155
+ it 'should update customer' do
156
+ should include('sbox')
157
+ end
158
+
159
+ it 'should reset customer' do
160
+ reset_customer.should_not include('sbox')
161
+ end
162
+ end
163
+ end
164
+
165
+ describe 'customer_edit_subscription' do
166
+ before { cg_client.customers_new(ValidCustomer) }
167
+ subject { cg_client.customers_edit_subscription({:customer_code => CustomerCode},{:subscription => ValidSubscription}).to_s }
168
+
169
+ it 'should update subscription' do
170
+ should include(PlanCodes.first)
171
+ end
172
+
173
+ describe 'customers_delete' do
174
+ subject { cg_client.customers_delete(:customer_code => CustomerCode).to_s }
175
+ it { should include 'success' }
176
+ end
177
+ end
178
+
179
+ describe 'customers_edit' do
180
+ before { sleep(8) }
181
+ before { cg_client.customers_new(ValidCustomer) }
182
+ subject { cg_client.customers_edit({:customer_code => CustomerCode},{:company => 'sbox', :subscription => ValidSubscription}).to_s }
183
+
184
+ it 'should update subscription' do
185
+ should include(PlanCodes.first)
186
+ end
187
+
188
+ it 'should update customer' do
189
+ should include('sbox')
190
+ end
191
+ end
192
+
193
+ describe 'customers_delete' do
194
+ subject { cg_client.customers_delete(:customer_code => CustomerCode).to_s }
195
+ it { should include 'success' }
196
+ end
197
+
198
+ context 'items' do
199
+ before { cg_client.customers_new(ValidCustomer) }
200
+
201
+ describe 'customers_add_item_quantity' do
202
+ context 'with a quantity' do
203
+ subject { cg_client.customers_add_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}, {:quantity => 37}).to_s }
204
+
205
+ it { should include '37' }
206
+ it { should include item_code }
207
+ end
208
+ end
209
+
210
+ describe 'set_item_quantity' do
211
+ subject { cg_client.customers_set_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}, {:quantity => 13}).to_s }
212
+
213
+ it { should include item_code }
214
+ it { should include '13' }
215
+ end
216
+
217
+ describe 'customers_add_item_quantity()' do
218
+ context 'with no quantity set' do
219
+ subject { cg_client.customers_add_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}).to_s }
220
+
221
+ it { should include item_code }
222
+ it { should include '14' }
223
+ end
224
+ end
225
+
226
+
227
+ describe 'remove_item_quantity' do
228
+ subject { cg_client.customers_remove_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}, {:quantity => 5}).to_s }
229
+
230
+ it { should include item_code }
231
+ it { should include '9' }
232
+
233
+ describe 'with no quantity set' do
234
+ subject { cg_client.customers_remove_item_quantity({:customer_code => CustomerCode, :itemCode => item_code}).to_s }
235
+
236
+ it { should include item_code }
237
+ it { should include '8' }
238
+ end
239
+ end
240
+
241
+ describe 'customers_delete' do
242
+ subject { cg_client.customers_delete(:customer_code => CustomerCode).to_s }
243
+ it { should include 'success' }
244
+ end
245
+ end
246
+
247
+ describe 'customers_add_charge' do
248
+ before { cg_client.customers_new(ValidCustomer) }
249
+ subject { cg_client.customers_add_charge({:customer_code => CustomerCode}, {:chargeCode => ChargeCode, :quantity => 1, :eachAmount => 113}).to_s }
250
+ it { should include ChargeCode }
251
+ it { should include '113' }
252
+
253
+ describe 'customers_delete' do
254
+ subject { cg_client.customers_delete(:customer_code => CustomerCode).to_s }
255
+ it { should include 'success' }
256
+ end
257
+ end
258
+
259
+ describe 'customers_delete_charge' do
260
+ before {
261
+ cg_client.customers_new(ValidCustomer)
262
+ cg_client.customers_add_charge({:customer_code => CustomerCode}, {:chargeCode => ChargeCode, :quantity => 1, :eachAmount => 113}).to_s
263
+ }
264
+
265
+ subject { cg_client.customers_delete_charge({:customer_code => CustomerCode}, {:chargeId => charge_id}) }
266
+
267
+ let(:charge_id) {
268
+ cg_client.customers_get({:customer_code => CustomerCode}).
269
+ parsed_response["customers"]["customer"]["subscriptions"]["subscription"]["invoices"]["invoice"]["charges"]["charge"].last["id"]
270
+ }
271
+ it { should_not include charge_id }
272
+ end
273
+
274
+ describe 'invoices_new' do
275
+ before { cg_client.customers_new(ValidCustomer) }
276
+ subject { cg_client.invoices_new(
277
+ {:customer_code => CustomerCode},
278
+ {:charges => {"1" => {:chargeCode => ChargeCode, :quantity => 1, :eachAmount => 117}}}
279
+ ).to_s }
280
+ it { should include ChargeCode }
281
+ it { should include '117' }
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path("lib/cheddargetter_client.rb")
2
+
3
+ RSpec.configure do |config|
4
+ # == Mock Framework
5
+ #
6
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
7
+ #
8
+ # config.mock_with :mocha
9
+ # config.mock_with :flexmock
10
+ # config.mock_with :rr
11
+ config.mock_with :rspec
12
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cheddargetter_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brent Wooden
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: crack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: jeweler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: httparty
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: 0.10.0
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: 0.10.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: crack
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.3.2
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 0.3.2
126
+ description: A more flexible solution for accessing the Cheddargetter API
127
+ email: brent.wooden@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files:
131
+ - LICENSE.txt
132
+ - README.md
133
+ files:
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - VERSION
140
+ - cheddargetter_client.gemspec
141
+ - lib/cheddargetter/client.rb
142
+ - lib/cheddargetter/response.rb
143
+ - lib/cheddargetter_client.rb
144
+ - pkg/cheddargetter_client-0.0.0.gem
145
+ - spec/cheddar_getter_client_spec.rb
146
+ - spec/spec_helper.rb
147
+ homepage: http://github.com/BrentW/cheddargetter_client
148
+ licenses:
149
+ - MIT
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ! '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ segments:
161
+ - 0
162
+ hash: 518553288699319148
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ! '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 1.8.24
172
+ signing_key:
173
+ specification_version: 3
174
+ summary: A ruby wrapper for the Cheddargetter API
175
+ test_files: []