lightspeed_pos 0.1.0 → 0.6.0
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.
- checksums.yaml +5 -5
- data/.mailmap +4 -0
- data/.rubocop.yml +32 -24
- data/.rubocop_todo.yml +284 -0
- data/.travis.yml +5 -3
- data/Gemfile +2 -0
- data/README.markdown +85 -28
- data/Rakefile +2 -0
- data/bin/console +33 -6
- data/lib/lightspeed/account.rb +50 -43
- data/lib/lightspeed/accounts.rb +24 -0
- data/lib/lightspeed/categories.rb +7 -5
- data/lib/lightspeed/category.rb +18 -7
- data/lib/lightspeed/client.rb +41 -27
- data/lib/lightspeed/collection.rb +214 -0
- data/lib/lightspeed/customer.rb +36 -0
- data/lib/lightspeed/customers.rb +11 -0
- data/lib/lightspeed/employee.rb +27 -0
- data/lib/lightspeed/employees.rb +10 -0
- data/lib/lightspeed/error.rb +17 -0
- data/lib/lightspeed/image.rb +37 -0
- data/lib/lightspeed/images.rb +18 -0
- data/lib/lightspeed/inventories.rb +10 -0
- data/lib/lightspeed/inventory.rb +14 -0
- data/lib/lightspeed/item.rb +55 -18
- data/lib/lightspeed/item_attribute_set.rb +15 -0
- data/lib/lightspeed/item_attribute_sets.rb +10 -0
- data/lib/lightspeed/item_matrices.rb +6 -3
- data/lib/lightspeed/item_matrix.rb +50 -10
- data/lib/lightspeed/items.rb +6 -7
- data/lib/lightspeed/order.rb +36 -0
- data/lib/lightspeed/orders.rb +12 -0
- data/lib/lightspeed/price_level.rb +16 -0
- data/lib/lightspeed/price_levels.rb +10 -0
- data/lib/lightspeed/prices.rb +45 -0
- data/lib/lightspeed/request.rb +98 -29
- data/lib/lightspeed/request_throttler.rb +33 -0
- data/lib/lightspeed/resource.rb +221 -0
- data/lib/lightspeed/sale.rb +59 -0
- data/lib/lightspeed/sale_line.rb +54 -0
- data/lib/lightspeed/sale_lines.rb +11 -0
- data/lib/lightspeed/sales.rb +12 -0
- data/lib/lightspeed/shop.rb +32 -0
- data/lib/lightspeed/shops.rb +10 -0
- data/lib/lightspeed/special_order.rb +24 -0
- data/lib/lightspeed/special_orders.rb +12 -0
- data/lib/lightspeed/vendor.rb +25 -0
- data/lib/lightspeed/vendors.rb +11 -0
- data/lib/lightspeed/version.rb +3 -1
- data/lib/lightspeed_pos.rb +5 -5
- data/lightspeed_pos.gemspec +11 -7
- data/script/buildkite +24 -0
- data/script/docker_tests +29 -0
- metadata +96 -38
- data/lib/lightspeed/account_resources.rb +0 -103
- data/lib/lightspeed/base.rb +0 -17
- data/lib/lightspeed/errors.rb +0 -8
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'resource'
|
4
|
+
|
5
|
+
module Lightspeed
|
6
|
+
class PriceLevel < Lightspeed::Resource
|
7
|
+
fields(
|
8
|
+
priceLevelID: :id,
|
9
|
+
name: :string,
|
10
|
+
archived: :boolean,
|
11
|
+
canBeArchived: :boolean,
|
12
|
+
type: :string,
|
13
|
+
Calculation: :String
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
require 'bigdecimal'
|
5
|
+
|
6
|
+
module Lightspeed
|
7
|
+
class Prices
|
8
|
+
def initialize(attributes)
|
9
|
+
@attributes = attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
def prices
|
13
|
+
@prices ||= @attributes["ItemPrice"].map { |v| [v["useType"].parameterize.underscore.to_sym, BigDecimal(v["amount"])] }.to_h
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json
|
17
|
+
attributes
|
18
|
+
end
|
19
|
+
alias_method :to_h, :as_json
|
20
|
+
|
21
|
+
def to_json
|
22
|
+
Yajl::Encoder.encode(as_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
prices[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
prices.inspect
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to?(method, private_method)
|
34
|
+
prices.keys.include?(method) || super
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
if prices.keys.include?(method)
|
39
|
+
prices[method]
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/lightspeed/request.rb
CHANGED
@@ -1,55 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
require 'net/http'
|
5
|
+
|
1
6
|
module Lightspeed
|
2
7
|
class Request
|
3
|
-
attr_accessor :raw_request
|
8
|
+
attr_accessor :raw_request, :bucket_max, :bucket_level
|
9
|
+
|
10
|
+
SECONDS_TO_WAIT_WHEN_THROTTLED = 60 # API requirements.
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :verbose
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.verbose?
|
17
|
+
!! @verbose
|
18
|
+
end
|
4
19
|
|
5
|
-
def self.
|
6
|
-
"
|
20
|
+
def self.base_host
|
21
|
+
"api.merchantos.com"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.base_path
|
25
|
+
"/API"
|
7
26
|
end
|
8
27
|
|
9
28
|
def initialize(client, method:, path:, params: nil, body: nil)
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
29
|
+
@method = method
|
30
|
+
@params = params
|
31
|
+
@path = path
|
32
|
+
@bucket_max = Float::INFINITY
|
33
|
+
@bucket_level = 0
|
34
|
+
@http = Net::HTTP.new(self.class.base_host, 443)
|
35
|
+
@http.use_ssl = true
|
36
|
+
@raw_request = request_class.new(uri)
|
37
|
+
@raw_request.body = body if body
|
38
|
+
@raw_request.set_form_data(@params) if @params && @method != :get
|
39
|
+
@client = client
|
40
|
+
set_authorization_header
|
41
|
+
end
|
22
42
|
|
23
|
-
|
43
|
+
def set_authorization_header
|
44
|
+
@raw_request["Authorization"] = "Bearer #{@client.oauth_token}" if @client.oauth_token
|
24
45
|
end
|
25
46
|
|
26
|
-
def
|
27
|
-
response = raw_request
|
28
|
-
|
47
|
+
def perform_raw
|
48
|
+
response = @http.request(raw_request)
|
49
|
+
extract_rate_limits(response)
|
50
|
+
if response.code == "200"
|
29
51
|
handle_success(response)
|
30
52
|
else
|
31
53
|
handle_error(response)
|
32
54
|
end
|
33
55
|
end
|
34
56
|
|
57
|
+
def perform
|
58
|
+
perform_raw
|
59
|
+
rescue Lightspeed::Error::Throttled
|
60
|
+
retry_throttled_request
|
61
|
+
rescue Lightspeed::Error::Unauthorized => e
|
62
|
+
raise e if @attempted_oauth_token_refresh
|
63
|
+
@client.refresh_oauth_token
|
64
|
+
set_authorization_header
|
65
|
+
@attempted_oauth_token_refresh = true
|
66
|
+
perform
|
67
|
+
end
|
68
|
+
|
35
69
|
private
|
36
70
|
|
37
71
|
def handle_success(response)
|
38
|
-
|
72
|
+
json = Yajl::Parser.parse(response.body)
|
73
|
+
pp json if self.class.verbose?
|
74
|
+
json
|
75
|
+
end
|
76
|
+
|
77
|
+
def retry_throttled_request
|
78
|
+
puts 'retrying throttled request after 60s.' if self.class.verbose?
|
79
|
+
sleep SECONDS_TO_WAIT_WHEN_THROTTLED
|
80
|
+
perform
|
39
81
|
end
|
40
82
|
|
41
83
|
def handle_error(response)
|
42
|
-
|
43
|
-
|
44
|
-
when
|
45
|
-
|
46
|
-
when
|
47
|
-
|
48
|
-
when
|
49
|
-
|
84
|
+
error = case response.code.to_s
|
85
|
+
when '400' then Lightspeed::Error::BadRequest
|
86
|
+
when '401' then Lightspeed::Error::Unauthorized
|
87
|
+
when '403' then Lightspeed::Error::NotAuthorized
|
88
|
+
when '404' then Lightspeed::Error::NotFound
|
89
|
+
when '429' then Lightspeed::Error::Throttled
|
90
|
+
when /5../ then Lightspeed::Error::InternalServerError
|
91
|
+
else Lightspeed::Error
|
50
92
|
end
|
51
93
|
|
52
|
-
|
94
|
+
# We may not get back valid JSON for a failed request
|
95
|
+
begin
|
96
|
+
data = Yajl::Parser.parse(response.body)
|
97
|
+
raise error, data["message"]
|
98
|
+
rescue Yajl::ParseError
|
99
|
+
raise error, response.code
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def extract_rate_limits(response)
|
104
|
+
if bucket_headers = response["X-LS-API-Bucket-Level"]
|
105
|
+
@bucket_level, @bucket_max = bucket_headers.split("/").map(&:to_f)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def uri
|
110
|
+
uri = self.class.base_path + @path
|
111
|
+
uri += "?#{URI.encode_www_form(@params)}" if @params && @method == :get
|
112
|
+
uri
|
113
|
+
end
|
114
|
+
|
115
|
+
def request_class
|
116
|
+
case @method
|
117
|
+
when :get then Net::HTTP::Get
|
118
|
+
when :put then Net::HTTP::Put
|
119
|
+
when :post then Net::HTTP::Post
|
120
|
+
when :delete then Net::HTTP::Delete
|
121
|
+
end
|
53
122
|
end
|
54
123
|
end
|
55
124
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightspeed
|
4
|
+
class RequestThrottler
|
5
|
+
attr_accessor :bucket_level, :bucket_max, :units_per_second
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@units_per_second = 0.5
|
9
|
+
@bucket_max = Float::INFINITY
|
10
|
+
@bucket_level = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform_request request
|
14
|
+
u = units request
|
15
|
+
sleep(u / @units_per_second) if @bucket_level + u > @bucket_max
|
16
|
+
response = request.perform
|
17
|
+
extract_rate_limits request
|
18
|
+
response
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def units request
|
24
|
+
if request.raw_request.is_a? Net::HTTP::Get then 1 else 10 end
|
25
|
+
end
|
26
|
+
|
27
|
+
def extract_rate_limits request
|
28
|
+
@bucket_max, @bucket_level = request.bucket_max, request.bucket_level
|
29
|
+
@units_per_second = @bucket_max/60.0
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'active_support/core_ext/string'
|
5
|
+
require 'active_support/core_ext/hash/slice'
|
6
|
+
|
7
|
+
require_relative 'collection'
|
8
|
+
|
9
|
+
module Lightspeed
|
10
|
+
class ID < Integer; end
|
11
|
+
class Link; end # rubocop:disable Lint/EmptyClass
|
12
|
+
class Resource
|
13
|
+
attr_accessor :id, :attributes, :client, :context, :account
|
14
|
+
|
15
|
+
def initialize(client: nil, context: nil, attributes: {})
|
16
|
+
self.client = client
|
17
|
+
self.context = context
|
18
|
+
self.attributes = attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def attributes=(attributes)
|
22
|
+
@attributes = attributes
|
23
|
+
attributes.each do |key, value|
|
24
|
+
send(:"#{key}=", value) if self.respond_to?(:"#{key}=")
|
25
|
+
end
|
26
|
+
self.id = send(self.class.id_field)
|
27
|
+
attributes
|
28
|
+
end
|
29
|
+
|
30
|
+
def account
|
31
|
+
@account || context.try(:account)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.fields(fields = {})
|
35
|
+
@fields ||= []
|
36
|
+
attr_writer(*fields.keys)
|
37
|
+
|
38
|
+
fields.each do |name, klass|
|
39
|
+
@fields << define_method(name) do
|
40
|
+
get_transformed_value(name, klass)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@fields
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_transformed_value(name, kind)
|
47
|
+
value = instance_variable_get("@#{name}")
|
48
|
+
if value.is_a?(String)
|
49
|
+
case kind
|
50
|
+
when :string then value
|
51
|
+
when :id, :integer then value.to_i
|
52
|
+
when :datetime then DateTime.parse(value)
|
53
|
+
when :boolean then value == 'true'
|
54
|
+
when :decimal then BigDecimal(value)
|
55
|
+
when :hash then Hash.new(value)
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Could not transform value #{value} to a #{kind}"
|
58
|
+
end
|
59
|
+
else
|
60
|
+
value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def client
|
65
|
+
@client || context.try(:client)
|
66
|
+
end
|
67
|
+
|
68
|
+
def load
|
69
|
+
self.attributes = get[resource_name] if (attributes.keys - [self.class.id_field]).empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def reload
|
73
|
+
self.attributes = get[resource_name]
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(attributes = {})
|
77
|
+
self.attributes = put(body: Yajl::Encoder.encode(attributes))[resource_name]
|
78
|
+
end
|
79
|
+
|
80
|
+
def destroy
|
81
|
+
self.attributes = delete[resource_name]
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.resource_name
|
86
|
+
name.demodulize
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.collection_name
|
90
|
+
resource_name.pluralize
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.id_field
|
94
|
+
"#{resource_name.camelize(:lower)}ID"
|
95
|
+
end
|
96
|
+
|
97
|
+
def id_params
|
98
|
+
{ self.class.id_field => id }
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.relationships(*args)
|
102
|
+
@relationships ||= []
|
103
|
+
paired_args = args.flat_map { |r| r.is_a?(Hash) ? r.to_a : [[r, r]] }
|
104
|
+
paired_args.each do |(relation_name, class_name)|
|
105
|
+
method_name = relation_name.to_s.underscore.to_sym
|
106
|
+
@relationships << define_method(method_name) do
|
107
|
+
instance_variable_get("@#{method_name}") || get_relation(method_name, relation_name, class_name)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
@relationships
|
111
|
+
end
|
112
|
+
|
113
|
+
def inspect
|
114
|
+
"#<#{self.class.name} API#{base_path}>"
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def to_json
|
119
|
+
Yajl::Encoder.encode(as_json)
|
120
|
+
end
|
121
|
+
|
122
|
+
def as_json
|
123
|
+
fields_to_h.merge(relationships_to_h).reject { |_, v| v.nil? || v == {} }
|
124
|
+
end
|
125
|
+
alias_method :to_h, :as_json
|
126
|
+
|
127
|
+
def base_path
|
128
|
+
if context.is_a?(Lightspeed::Collection)
|
129
|
+
"#{context.base_path}/#{id}"
|
130
|
+
else
|
131
|
+
"#{account.base_path}/#{resource_name}/#{id}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def singular_path_parent
|
136
|
+
context
|
137
|
+
end
|
138
|
+
|
139
|
+
def resource_name
|
140
|
+
self.class.resource_name
|
141
|
+
end
|
142
|
+
|
143
|
+
def read_attribute_for_serialization(method_name)
|
144
|
+
method_name = method_name.to_sym
|
145
|
+
|
146
|
+
if self.class.fields.include?(method_name) || self.class.relationships.include?(method_name)
|
147
|
+
send(method_name)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def fields_to_h
|
154
|
+
self.class.fields.map { |f| [f, send(f)] }.to_h
|
155
|
+
end
|
156
|
+
|
157
|
+
def relationships_to_h
|
158
|
+
self.class.relationships.map { |r| [r.to_s.camelize, send(r).to_h] }.to_h
|
159
|
+
end
|
160
|
+
|
161
|
+
def collection_class
|
162
|
+
"Lightspeed::#{self.class.collection_name}".constantize
|
163
|
+
end
|
164
|
+
|
165
|
+
def get_relation(method_name, relation_name, class_name)
|
166
|
+
klass = "Lightspeed::#{class_name}".constantize
|
167
|
+
case
|
168
|
+
when klass <= Lightspeed::Collection then get_collection_relation(method_name, relation_name, klass)
|
169
|
+
when klass <= Lightspeed::Resource then get_resource_relation(method_name, relation_name, klass)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_collection_relation(method_name, relation_name, klass)
|
174
|
+
collection = klass.new(context: self, attributes: attributes[relation_name.to_s])
|
175
|
+
instance_variable_set("@#{method_name}", collection)
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_resource_relation(method_name, relation_name, klass)
|
179
|
+
id_field = "#{relation_name.to_s.camelize(:lower)}ID" # parentID != #categoryID, so we can't use klass.id_field
|
180
|
+
resource = if send(id_field).to_i.nonzero?
|
181
|
+
rel_attributes = attributes[klass.resource_name] || { klass.id_field => send(id_field) }
|
182
|
+
klass.new(context: self, attributes: rel_attributes).tap(&:load)
|
183
|
+
end
|
184
|
+
instance_variable_set("@#{method_name}", resource)
|
185
|
+
end
|
186
|
+
|
187
|
+
def context_params
|
188
|
+
if context.respond_to?(:id_field) &&
|
189
|
+
respond_to?(context.id_field.to_sym)
|
190
|
+
{ context.id_field => context.id }
|
191
|
+
else
|
192
|
+
{}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def get(params: {})
|
197
|
+
params = { load_relations: 'all' }.merge(context_params).merge(params)
|
198
|
+
client.get(
|
199
|
+
path: resource_path,
|
200
|
+
params: params
|
201
|
+
)
|
202
|
+
end
|
203
|
+
|
204
|
+
def put(body:)
|
205
|
+
client.put(
|
206
|
+
path: resource_path,
|
207
|
+
body: body
|
208
|
+
)
|
209
|
+
end
|
210
|
+
|
211
|
+
def delete
|
212
|
+
client.delete(
|
213
|
+
path: resource_path
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
def resource_path
|
218
|
+
"#{base_path}.json"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|