kounta_rest 0.1.7 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -36
  3. data/lib/kounta/address.rb +14 -17
  4. data/lib/kounta/break.rb +6 -0
  5. data/lib/kounta/category.rb +12 -15
  6. data/lib/kounta/company.rb +59 -50
  7. data/lib/kounta/customer.rb +33 -32
  8. data/lib/kounta/errors.rb +8 -7
  9. data/lib/kounta/inventory.rb +5 -7
  10. data/lib/kounta/line.rb +18 -21
  11. data/lib/kounta/order.rb +45 -49
  12. data/lib/kounta/payment.rb +14 -17
  13. data/lib/kounta/person.rb +21 -24
  14. data/lib/kounta/price_list.rb +7 -9
  15. data/lib/kounta/product.rb +25 -27
  16. data/lib/kounta/register.rb +11 -14
  17. data/lib/kounta/resource.rb +105 -93
  18. data/lib/kounta/rest/client.rb +177 -79
  19. data/lib/kounta/shift.rb +25 -0
  20. data/lib/kounta/site.rb +46 -38
  21. data/lib/kounta/staff.rb +30 -0
  22. data/lib/kounta/tax.rb +7 -9
  23. data/lib/kounta/version.rb +1 -1
  24. data/lib/kounta/webhook.rb +16 -0
  25. data/lib/kounta.rb +42 -72
  26. metadata +77 -145
  27. data/.gitignore +0 -20
  28. data/.gitmodules +0 -3
  29. data/Gemfile +0 -4
  30. data/Rakefile +0 -1
  31. data/console.rb +0 -14
  32. data/kounta.gemspec +0 -33
  33. data/spec/fixtures/address.json +0 -12
  34. data/spec/fixtures/addresses.json +0 -25
  35. data/spec/fixtures/base_price_list.json +0 -52
  36. data/spec/fixtures/categories.json +0 -217
  37. data/spec/fixtures/category.json +0 -11
  38. data/spec/fixtures/companies_me.json +0 -45
  39. data/spec/fixtures/customer.json +0 -39
  40. data/spec/fixtures/customers.json +0 -26
  41. data/spec/fixtures/inventory.json +0 -10
  42. data/spec/fixtures/line.json +0 -7
  43. data/spec/fixtures/order.json +0 -67
  44. data/spec/fixtures/orders.json +0 -134
  45. data/spec/fixtures/payment.json +0 -6
  46. data/spec/fixtures/people.json +0 -20
  47. data/spec/fixtures/person.json +0 -41
  48. data/spec/fixtures/price_list.json +0 -52
  49. data/spec/fixtures/price_lists.json +0 -12
  50. data/spec/fixtures/product.json +0 -25
  51. data/spec/fixtures/products.json +0 -119
  52. data/spec/fixtures/site.json +0 -6
  53. data/spec/fixtures/sites.json +0 -14
  54. data/spec/fixtures/tax.json +0 -7
  55. data/spec/fixtures/taxes.json +0 -16
  56. data/spec/helper.rb +0 -28
  57. data/spec/kounta/address_spec.rb +0 -11
  58. data/spec/kounta/category_spec.rb +0 -11
  59. data/spec/kounta/company_spec.rb +0 -24
  60. data/spec/kounta/customer_spec.rb +0 -15
  61. data/spec/kounta/kounta_spec.rb +0 -21
  62. data/spec/kounta/line_spec.rb +0 -14
  63. data/spec/kounta/order_spec.rb +0 -37
  64. data/spec/kounta/payment_spec.rb +0 -14
  65. data/spec/kounta/person_spec.rb +0 -15
  66. data/spec/kounta/product_spec.rb +0 -16
  67. data/spec/kounta/resource_spec.rb +0 -95
  68. data/spec/kounta/rest/client_spec.rb +0 -38
  69. data/spec/kounta/site_spec.rb +0 -11
  70. data/spec/support/endpoints.rb +0 -72
@@ -1,10 +1,8 @@
1
1
  module Kounta
2
-
3
- class PriceList < Kounta::Resource
4
- property :parent_id
5
- property :name
6
- property :products
7
- coerce_key :products, Product
8
- end
9
-
10
- end
2
+ class PriceList < Kounta::Resource
3
+ property :parent_id
4
+ property :name
5
+ property :products
6
+ coerce_key :products, Product
7
+ end
8
+ end
@@ -1,31 +1,29 @@
1
1
  module Kounta
2
+ class Product < Kounta::Resource
3
+ property :company_id
4
+ property :code
5
+ property :barcode
6
+ property :stock
7
+ property :name
8
+ property :description
9
+ property :tags
10
+ property :image
11
+ property :unit_price
12
+ property :cost_price
13
+ property :taxes
14
+ property :sites
15
+ property :number
2
16
 
3
- class Product < Kounta::Resource
4
- property :company_id
5
- property :code
6
- property :barcode
7
- property :stock
8
- property :name
9
- property :description
10
- property :tags
11
- property :image
12
- property :unit_price
13
- property :cost_price
14
- property :taxes
15
- property :sites
16
- property :number
17
+ has_many(:categories, Kounta::Category, { company_id: :company_id },
18
+ proc { |klass| { companies: klass.company_id, products: klass.id, categories: nil } })
19
+ coerce_key :taxes, Kounta::Tax
17
20
 
18
- has_many :categories, Kounta::Category, {:company_id => :company_id}, lambda { |klass| {companies: klass.company_id, products: klass.id, categories: nil} }
19
- coerce_key :taxes, Kounta::Tax
21
+ def tags_include?(name)
22
+ tags.any? { |s| s.casecmp(name).zero? }
23
+ end
20
24
 
21
- def tags_include?(name)
22
- tags.any?{ |s| s.casecmp(name) == 0 }
23
- end
24
-
25
- def resource_path
26
- {companies: company_id, products: id}
27
- end
28
-
29
- end
30
-
31
- end
25
+ def resource_path
26
+ { companies: company_id, products: id }
27
+ end
28
+ end
29
+ end
@@ -1,15 +1,12 @@
1
1
  module Kounta
2
-
3
- class Register < Kounta::Resource
4
- property :company_id
5
- property :code
6
- property :name
7
- property :site_id
8
-
9
- def resource_path
10
- {companies: company_id, registers: id}
11
- end
12
-
13
- end
14
-
15
- end
2
+ class Register < Kounta::Resource
3
+ property :company_id
4
+ property :code
5
+ property :name
6
+ property :site_id
7
+
8
+ def resource_path
9
+ { companies: company_id, registers: id }
10
+ end
11
+ end
12
+ end
@@ -1,96 +1,108 @@
1
1
  require 'hashie'
2
- require_relative "rest/client"
2
+ require_relative 'rest/client'
3
3
 
4
4
  module Kounta
5
-
6
- class Resource < Hashie::Dash
7
- include Hashie::Extensions::Coercion
8
-
9
- property :id
10
- property :created_at
11
- property :updated_at
12
-
13
- def self.coerce(data)
14
- if data.kind_of? Array
15
- data.map { |item| self.new(item) }
16
- else
17
- self.new(data)
18
- end
19
- end
20
-
21
- def self.has_one(sym, klass, assignments, route)
22
- define_method(sym) do |item_id=nil, *args|
23
- if item_id
24
- assign_into(client.object_from_response(klass, :get, route.call(self, item_id), {:params => args[0]}), self, assignments)
25
- else
26
- assign_into(klass.new, self, assignments)
27
- end
28
- end
29
- end
30
-
31
- def self.has_many(sym, klass, assignments, route)
32
- define_method(sym) do |*args|
33
- client.objects_from_response(klass, :get, route.call(self), {:params => args[0]}).map {|returned_klass| assign_into(returned_klass, self, assignments) }
34
- end
35
- end
36
-
37
- def initialize(hash={})
38
- if hash
39
- hash.each_pair do |k,v|
40
- begin
41
- self[k] = v if self.respond_to? k.to_sym
42
- rescue NoMethodError => e
43
- raise Kounta::Errors::UnknownResourceAttribute.new("Unknown attribute: #{k} on resource #{self.class}")
44
- end
45
- end
46
- end
47
- end
48
-
49
- def client
50
- @@client ||= Kounta::REST::Client.new
51
- end
52
-
53
- def to_hash(hash={})
54
- {}.tap do |returning|
55
- self.class.properties.each do |property|
56
- next if ignored_properties.include?(property)
57
- returning[property] = self[property] if self[property]
58
- end
59
- end.merge(hash)
60
- end
61
-
62
- def save!
63
- response = new? ? client.perform(resource_path, :post, {:body => to_hash}) : client.perform(resource_path, :put, {:body => to_hash})
64
-
65
- # automatically follow redirects to resources
66
- if response.status == 201
67
- response = client.perform(response.headers["location"], :get)
68
- end
69
-
70
- response.parsed.each_pair do |k,v|
71
- self[k] = v if self.respond_to? k.to_sym
72
- end
73
-
74
- self
75
- end
76
-
77
- def new?
78
- !id
79
- end
80
-
81
- def ignored_properties(array=[])
82
- array + [:created_at, :updated_at, :id, :company_id]
83
- end
84
-
85
- private
86
-
87
- def assign_into(klass, assigner, assignments)
88
- assignments.each_pair do |k,v|
89
- klass[k] = assigner[v]
90
- end
91
- klass
92
- end
93
-
94
- end
95
-
96
- end
5
+ class Resource < Hashie::Dash
6
+ include Hashie::Extensions::Dash::IndifferentAccess if defined?(Hashie::Extensions::Dash::IndifferentAccess)
7
+ include Hashie::Extensions::Coercion
8
+
9
+ attr_accessor :client
10
+
11
+ property :id
12
+ property :created_at
13
+ property :updated_at
14
+
15
+ def self.coerce(data)
16
+ if data.is_a? Array
17
+ data.map { |item| new(item) }
18
+ else
19
+ new(data)
20
+ end
21
+ end
22
+
23
+ def self.has_one(sym, klass, assignments, route) # rubocop:disable Naming/PredicateName
24
+ define_method(sym) do |item_id = nil, *args|
25
+ if item_id
26
+ assign_into(client.object_from_response(klass, :get, route.call(self, item_id), params: args[0]), self, assignments)
27
+ else
28
+ assign_into(klass.new, self, assignments)
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.has_many(sym, klass, assignments, route) # rubocop:disable Naming/PredicateName
34
+ define_method(sym) do |has_many_params = nil, *args|
35
+ client.objects_from_response(klass, :get, route.call(self, has_many_params), params: args[0])
36
+ .map { |returned_klass| assign_into(returned_klass, self, assignments) }
37
+ end
38
+ end
39
+
40
+ def self.has_many_in_time_range(sym, klass, assignments, route) # rubocop:disable Naming/PredicateName
41
+ define_method(sym) do |has_many_params = nil, *args|
42
+ client.objects_from_response_in_time_range(klass, :get, route.call(self, has_many_params), params: args[0])
43
+ .map { |returned_klass| assign_into(returned_klass, self, assignments) }
44
+ end
45
+ end
46
+
47
+ def initialize(hash = {})
48
+ return unless hash
49
+
50
+ hash.each_pair do |k, v|
51
+ begin
52
+ self[k] = v if respond_to? k.to_sym
53
+ rescue NoMethodError
54
+ raise Kounta::Errors::UnknownResourceAttribute, "Unknown attribute: #{k} on resource #{self.class}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def to_hash(hash = {})
60
+ {}.tap do |returning|
61
+ self.class.properties.each do |property|
62
+ next if ignored_properties.include?(property)
63
+ returning[property] = self[property] if self[property]
64
+ end
65
+ end.merge(hash)
66
+ end
67
+
68
+ def save!
69
+ response = new? ? client.perform(resource_path, :post, body: to_hash) : client.perform(resource_path, :put, body: to_hash)
70
+
71
+ # automatically follow redirects to resources
72
+ response = client.perform(response.headers['location'], :get) if response.status == 201
73
+
74
+ response.parsed.each_pair do |k, v|
75
+ self[k] = v if respond_to? k.to_sym
76
+ end
77
+
78
+ self
79
+ end
80
+
81
+ def delete!
82
+ return self if new?
83
+ response = client.perform(resource_path, :delete)
84
+
85
+ except!('id', 'created_at', 'updated_at') if response.status == 204
86
+
87
+ self
88
+ end
89
+
90
+ def new?
91
+ !id
92
+ end
93
+
94
+ def ignored_properties(array = [])
95
+ array + %i[created_at updated_at id company_id]
96
+ end
97
+
98
+ private
99
+
100
+ def assign_into(klass, assigner, assignments)
101
+ klass.client = assigner.client
102
+ assignments.each_pair do |k, v|
103
+ klass[k] = assigner[v]
104
+ end
105
+ klass
106
+ end
107
+ end
108
+ end
@@ -3,82 +3,180 @@ require 'oj'
3
3
  require 'faraday_middleware'
4
4
 
5
5
  module Kounta
6
- module REST
7
- class Client
8
-
9
- def initialize
10
- raise Kounta::Errors::MissingOauthDetails unless has_required_oauth_details?
11
- @conn = OAuth2::AccessToken.new(client, Kounta.client_token, {:refresh_token => Kounta.client_refresh_token})
12
- end
13
-
14
- def client
15
- @oauth_client ||= OAuth2::Client.new(Kounta.client_id, Kounta.client_secret, {
16
- :site => Kounta::SITE_URI,
17
- :authorize_url => Kounta::AUTHORIZATION_URI,
18
- :token_url => Kounta::TOKEN_URI
19
- }) do |faraday|
20
- faraday.request :json
21
- faraday.use Faraday::Request::UrlEncoded
22
- faraday.use Faraday::Response::Logger if Kounta.enable_logging
23
- faraday.adapter Faraday.default_adapter
24
- end
25
- end
26
-
27
- def path_from_hash(url_hash)
28
- url_hash.map{ |key, value| value ? "#{key}/#{value}" : "#{key}" }.join('/')
29
- end
30
-
31
- def perform(url_hash, request_method, options={})
32
- begin
33
- if url_hash.kind_of? Hash
34
- response = @conn.request(request_method, "#{path_from_hash(url_hash)}.#{FORMAT.to_s}", options)
35
- else
36
- response = @conn.request(request_method, url_hash, options)
37
- end
38
- rescue OAuth2::Error => ex
39
- puts "-- in error"
40
- puts response.inspect
41
- if ex.message.include?('expired') || ex.message.include?('invalid')
42
- @conn = refreshed_token
43
- retry
44
- end
45
-
46
- #raise Kounta::Errors::APIError.new(ex.message)
47
- end
48
- response
49
- end
50
-
51
- def objects_from_response(klass, request_method, url_hash, options={})
52
- response = perform(url_hash, request_method, options)
53
- page_count = response.headers["x-pages"].to_i
54
-
55
- if page_count > 1
56
- results = []
57
- page_count.times { |page_number|
58
- response = perform(url_hash, request_method, options.merge!(:headers => {'X-Page' => (page_number).to_s}))
59
- results = results + response.parsed
60
- }
61
- results.map { |item| klass.new(item) }
62
- else
63
- response.parsed.map { |item| klass.new(item) }
64
- end
65
- end
66
-
67
- def object_from_response(klass, request_method, url_hash, options={})
68
- response = perform(url_hash, request_method, options)
69
- klass.new(response.parsed)
70
- end
71
-
72
- private
73
-
74
- def has_required_oauth_details?
75
- Kounta.client_id && Kounta.client_secret && Kounta.client_token && Kounta.client_refresh_token
76
- end
77
-
78
- def refreshed_token
79
- OAuth2::AccessToken.from_hash(client, :refresh_token => Kounta.client_refresh_token).refresh!
80
- end
81
-
82
- end
83
- end
84
- end
6
+ module REST
7
+ class Client
8
+ def initialize(**options)
9
+ @redirect_uri = options[:redirect_uri]
10
+ @consumer = options[:consumer]
11
+ @access_token = options[:access_token]
12
+ @refresh_token = options[:refresh_token]
13
+ @client = OAuth2::Client.new(
14
+ @consumer[:key], @consumer[:secret],
15
+ site: Kounta::SITE_URI,
16
+ authorize_url: Kounta::AUTHORIZATION_URI,
17
+ token_url: Kounta::TOKEN_URI
18
+ ) do |faraday|
19
+ faraday.request :json
20
+ faraday.use Faraday::Request::UrlEncoded
21
+ faraday.use Faraday::Response::Logger if Kounta.enable_logging
22
+ faraday.adapter Faraday.default_adapter
23
+ end
24
+ end
25
+
26
+ def authenticated?
27
+ @access_token.present?
28
+ end
29
+
30
+ def get_access_code_url(params = {})
31
+ # Kounta's API seems to require the `state` param (can't find documentation on it anywhere)
32
+ # learn more about it: http://homakov.blogspot.com.au/2012/07/saferweb-most-common-oauth2.html
33
+ @client.auth_code.authorize_url(params.merge(redirect_uri: @redirect_uri, state: SecureRandom.hex(24)))
34
+ end
35
+
36
+ def get_access_token(access_code)
37
+ @token = @client.auth_code.get_token(access_code, redirect_uri: @redirect_uri)
38
+ @access_token = @token.token
39
+ @expires_at = @token.expires_at
40
+ @refresh_token = @token.refresh_token
41
+ @token
42
+ end
43
+
44
+ def company(hash = {})
45
+ @company ||= Kounta::Company.new(self, hash)
46
+ end
47
+
48
+ def path_from_hash(url_hash)
49
+ # TODO: there's probably a more correct way of doing this encoding
50
+ url_hash.map { |key, value| value ? "#{key}/#{value.to_s.gsub('-', '%2D')}" : key.to_s }.join('/')
51
+ end
52
+
53
+ def perform(url_hash, request_method, options = {})
54
+ begin
55
+ response = if url_hash.is_a? Hash
56
+ oauth_connection.request(request_method, "#{path_from_hash(url_hash)}.#{FORMAT}", options)
57
+ else
58
+ oauth_connection.request(request_method, url_hash, options)
59
+ end
60
+ rescue Exception => ex # rubocop:disable Lint/RescueException
61
+ msg = ex.message
62
+ if !msg.nil? && (msg.include?('The access token provided has expired') || msg.include?('expired') || msg.include?('invalid'))
63
+ @oauth_connection = refreshed_token
64
+ retry
65
+ end
66
+
67
+ raise Kounta::Errors::RequestError, response.nil? ? 'Unknown Status' : response.status
68
+ end
69
+
70
+ raise Kounta::Errors::RequestError, 'Unknown Status' unless response
71
+
72
+ response
73
+ end
74
+
75
+ def objects_from_response(klass, request_method, url_hash, options = {})
76
+ response = perform(url_hash, request_method, options)
77
+ last_page = response.headers['x-pages'].to_i - 1
78
+ results = response.parsed
79
+
80
+ # Already got page 0, start at page 1
81
+ (1..last_page).each do |page_number|
82
+ response = perform(url_hash, request_method, options.merge!(headers: { 'X-Page' => page_number.to_s }))
83
+ results += response.parsed
84
+ end
85
+
86
+ results.map { |item| klass.new(item) }
87
+ end
88
+
89
+ def object_from_response(klass, request_method, url_hash, options = {})
90
+ response = perform(url_hash, request_method, options)
91
+ klass.new(response.parsed)
92
+ end
93
+
94
+ def objects_from_response_in_time_range(klass, request_method, url_hash, options = {})
95
+ if options.key?(:params) && (options[:params].key?(:created_gte) ^ options[:params].key?(:created_gt)) &&
96
+ (options[:params].key?(:created_lte) ^ options[:params].key?(:created_lt))
97
+ params = options[:params]
98
+ start_equal = params.key?(:created_gte)
99
+ finish_equal = params.key?(:created_lte)
100
+ start = (start_equal ? params[:created_gte] : params[:created_gt]).to_time
101
+ finish = (finish_equal ? params[:created_lte] : params[:created_lt]).to_time
102
+
103
+ params.except!(:created_gte, :created_gt, :created_lte, :created_lt)
104
+
105
+ start_date = start.to_date
106
+ finish_date = finish.to_date
107
+
108
+ start = start.to_i
109
+ finish = finish.to_i
110
+
111
+ results = []
112
+
113
+ options[:params] = params.merge(created_gte: start_date, created_lte: finish_date)
114
+ response = perform(url_hash, request_method, options)
115
+ last_page = response.headers['x-pages'].to_i - 1
116
+ parsed = response.parsed.map do |o|
117
+ o['created_at_epoch'] = Time.parse(o['created_at']).to_i
118
+ o
119
+ end
120
+
121
+ response_start = parsed.last['created_at_epoch']
122
+ response_finish = parsed.first['created_at_epoch']
123
+
124
+ # all current and future responses will be before the required start
125
+ return results if response_finish < start
126
+
127
+ # reverse to order them in ascending order
128
+ results += filter_responses_in_date_range(parsed.reverse, start, finish)
129
+
130
+ # all future responses will be before the required start
131
+ return results if response_start < start
132
+
133
+ (1..last_page).each do |page_number|
134
+ response = perform(url_hash, request_method, options.merge!(headers: { 'X-Page' => page_number.to_s }))
135
+ parsed = response.parsed.map do |o|
136
+ o['created_at_epoch'] = Time.parse(o['created_at']).to_i
137
+ o
138
+ end
139
+
140
+ response_start = parsed.last['created_at_epoch']
141
+ response_finish = parsed.first['created_at_epoch']
142
+
143
+ # all current and future responses will be before the required start
144
+ break if response_finish < start
145
+
146
+ # reverse to order them in ascending order
147
+ results += filter_responses_in_date_range(parsed.reverse, start, finish)
148
+
149
+ # all future responses will be before the required start
150
+ break if response_start < start
151
+ end
152
+
153
+ # reverse to under reverses used to order them in ascending order
154
+ results.reverse.map { |item| klass.new(item) }
155
+ else
156
+ raise ArgumentError, 'url_has must contain exactly one of [:created_gte, :created_gt] ' \
157
+ 'and exactly one of [:created_lte, :created_lt]'
158
+ end
159
+ end
160
+
161
+ def filter_responses_in_date_range(parsed_responses, start, finish)
162
+ start_index = parsed_responses.index { |response| response['created_at_epoch'] >= start } || parsed_responses.length - 1
163
+ finish_index = parsed_responses.rindex { |response| response['created_at_epoch'] <= finish } || 0
164
+ parsed_responses[start_index..finish_index]
165
+ end
166
+
167
+ private
168
+
169
+ def oauth_connection
170
+ @oauth_connection ||= if @refresh_token
171
+ OAuth2::AccessToken.new(@client, @access_token, refresh_token: @refresh_token).refresh!
172
+ else
173
+ OAuth2::AccessToken.new(@client, @access_token)
174
+ end
175
+ end
176
+
177
+ def refreshed_token
178
+ OAuth2::AccessToken.from_hash(@client, refresh_token: @refresh_token).refresh!
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,25 @@
1
+ module Kounta
2
+ class Shift < Kounta::Resource
3
+ property :company_id
4
+ property :staff_member
5
+ property :site
6
+ property :started_at
7
+ property :finished_at
8
+ property :breaks
9
+
10
+ # coerce_key :staff_member, Kounta::Staff
11
+ # coerce_key :site, Kounta::Site
12
+ coerce_key :breaks, Kounta::Break
13
+
14
+ def initialize(hash = {})
15
+ super(hash)
16
+ self.breaks ||= []
17
+ end
18
+
19
+ def to_hash
20
+ returning = {}
21
+ returning[:breaks] = breaks.map(&:to_hash) if breaks && !breaks.empty?
22
+ super(returning)
23
+ end
24
+ end
25
+ end