kounta_rest 0.1.7 → 0.2.4

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