lightspeed_pos 0.1.0 → 0.2.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.markdown +57 -29
  3. data/bin/console +15 -7
  4. data/lib/lightspeed/account.rb +44 -43
  5. data/lib/lightspeed/accounts.rb +22 -0
  6. data/lib/lightspeed/categories.rb +5 -5
  7. data/lib/lightspeed/category.rb +16 -7
  8. data/lib/lightspeed/client.rb +37 -26
  9. data/lib/lightspeed/collection.rb +218 -0
  10. data/lib/lightspeed/employee.rb +25 -0
  11. data/lib/lightspeed/employees.rb +8 -0
  12. data/lib/lightspeed/{errors.rb → error.rb} +2 -1
  13. data/lib/lightspeed/image.rb +35 -0
  14. data/lib/lightspeed/images.rb +16 -0
  15. data/lib/lightspeed/inventories.rb +8 -0
  16. data/lib/lightspeed/inventory.rb +12 -0
  17. data/lib/lightspeed/item.rb +54 -18
  18. data/lib/lightspeed/item_attribute_set.rb +13 -0
  19. data/lib/lightspeed/item_attribute_sets.rb +8 -0
  20. data/lib/lightspeed/item_matrices.rb +4 -3
  21. data/lib/lightspeed/item_matrix.rb +48 -10
  22. data/lib/lightspeed/items.rb +4 -7
  23. data/lib/lightspeed/order.rb +34 -0
  24. data/lib/lightspeed/orders.rb +10 -0
  25. data/lib/lightspeed/prices.rb +43 -0
  26. data/lib/lightspeed/request.rb +74 -28
  27. data/lib/lightspeed/request_throttler.rb +31 -0
  28. data/lib/lightspeed/resource.rb +221 -0
  29. data/lib/lightspeed/sale.rb +57 -0
  30. data/lib/lightspeed/sale_line.rb +52 -0
  31. data/lib/lightspeed/sale_lines.rb +9 -0
  32. data/lib/lightspeed/sales.rb +10 -0
  33. data/lib/lightspeed/shop.rb +30 -0
  34. data/lib/lightspeed/shops.rb +8 -0
  35. data/lib/lightspeed/special_order.rb +22 -0
  36. data/lib/lightspeed/special_orders.rb +10 -0
  37. data/lib/lightspeed/vendor.rb +23 -0
  38. data/lib/lightspeed/vendors.rb +9 -0
  39. data/lib/lightspeed/version.rb +1 -1
  40. data/lib/lightspeed_pos.rb +2 -4
  41. data/lightspeed_pos.gemspec +3 -3
  42. data/script/buildkite +11 -0
  43. data/script/docker_tests +29 -0
  44. metadata +63 -24
  45. data/lib/lightspeed/account_resources.rb +0 -103
  46. data/lib/lightspeed/base.rb +0 -17
@@ -0,0 +1,25 @@
1
+ require_relative 'resource'
2
+
3
+ module Lightspeed
4
+ class Employee < Lightspeed::Resource
5
+ fields(
6
+ employeeID: :id,
7
+ firstName: :string,
8
+ lastName: :string,
9
+ accessPin: :string,
10
+ lockOut: :boolean,
11
+ archived: :boolean,
12
+ contactID: :integer,
13
+ clockInEmployeeHoursID: :integer,
14
+ employeeRoleID: :integer,
15
+ limitToShopID: :integer,
16
+ lastShopID: :integer,
17
+ lastSaleID: :integer,
18
+ lastRegisterID: :integer,
19
+ timeStamp: :datetime,
20
+ Contact: :hash,
21
+ EmployeeRole: :hash,
22
+ EmployeeRights: :hash
23
+ )
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'collection'
2
+
3
+ require_relative 'employee'
4
+
5
+ module Lightspeed
6
+ class Employees < Lightspeed::Collection
7
+ end
8
+ end
@@ -1,8 +1,9 @@
1
1
  module Lightspeed
2
- module Errors
2
+ class Error < Exception
3
3
  class BadRequest < Exception; end # 400
4
4
  class Unauthorized < Exception; end # 401
5
5
  class NotFound < Exception; end # 404
6
6
  class InternalServerError < Exception; end # 500
7
+ class Throttled < Exception; end # 503
7
8
  end
8
9
  end
@@ -0,0 +1,35 @@
1
+ require_relative 'resource'
2
+
3
+ require_relative 'item'
4
+ require_relative 'item_matrix'
5
+
6
+ module Lightspeed
7
+ class Image < Lightspeed::Resource
8
+ fields(
9
+ imageID: :id,
10
+ description: :string,
11
+ filename: :string,
12
+ baseImageURL: :string,
13
+ publicID: :string, # part of the file path; not a Lightspeed ID
14
+ itemID: :id,
15
+ itemMatrixID: :id,
16
+ Item: :hash,
17
+ ItemMatrix: :hash
18
+ )
19
+
20
+ relationships :Item, :ItemMatrix
21
+
22
+ def url
23
+ "#{baseImageURL}#{publicID}"
24
+ end
25
+
26
+ def base_path
27
+ if context.is_a?(Lightspeed::Item) ||
28
+ context.is_a?(Lightspeed::ItemMatrix)
29
+ "#{context.base_path}/#{resource_name}/#{id}"
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'collection'
2
+
3
+ require_relative 'image'
4
+
5
+ module Lightspeed
6
+ class Images < Lightspeed::Collection
7
+ def base_path
8
+ if context.is_a?(Lightspeed::Item) ||
9
+ context.is_a?(Lightspeed::ItemMatrix)
10
+ "#{context.base_path}/#{resource_name}"
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'collection'
2
+
3
+ require_relative 'inventory'
4
+
5
+ module Lightspeed
6
+ class Inventories < Lightspeed::Collection
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'resource'
2
+
3
+ require_relative 'item'
4
+
5
+ module Lightspeed
6
+ class Inventory < Lightspeed::Resource
7
+ fields(
8
+ inventoryID: :integer
9
+ )
10
+ relationships :Item
11
+ end
12
+ end
@@ -1,26 +1,62 @@
1
- require 'lightspeed/base'
2
1
 
3
- module Lightspeed
4
- class Item < Lightspeed::Base
5
- attr_accessor :systemSku, :defaultCost, :avgCost, :discountable, :tax,
6
- :archived, :itemType, :description, :modelYear, :upc, :ean, :customSku,
7
- :manufacturerSku, :createTime, :timeStamp,
2
+ require_relative 'resource'
3
+
4
+ require_relative 'item_matrix'
5
+ require_relative 'category'
6
+ require_relative 'images'
7
+ require_relative 'prices'
8
8
 
9
- # Association keys
10
- :categoryID, :taxClassID, :departmentID, :itemMatrixID, :manufacturerID, :seasonID,
11
- :defaultVendorID, :itemECommerceID,
9
+ module Lightspeed
10
+ class Item < Lightspeed::Resource
11
+ alias_method :archive, :destroy
12
12
 
13
- # Embedded
14
- :ItemMatrix, :ItemAttributes, :ItemShops, :Prices, :Note
13
+ fields(
14
+ itemID: :id,
15
+ systemSku: :string,
16
+ defaultCost: :decimal,
17
+ avgCost: :decimal,
18
+ discountable: :boolean,
19
+ tax: :boolean,
20
+ archived: :boolean,
21
+ itemType: :string,
22
+ description: :string,
23
+ modelYear: :integer,
24
+ upc: :string,
25
+ ean: :string,
26
+ customSku: :string,
27
+ manufacturerSku: :string,
28
+ createTime: :datetime,
29
+ timeStamp: :datetime,
30
+ categoryID: :id,
31
+ taxClassID: :id,
32
+ departmentID: :id,
33
+ itemMatrixID: :id,
34
+ manufacturerID: :id,
35
+ seasonID: :id,
36
+ defaultVendorID: :id,
37
+ itemECommerceID: :id,
38
+ # Category: :hash,
39
+ TaxClass: :hash,
40
+ Department: :hash,
41
+ ItemAttributes: :hash,
42
+ # ItemMatrix: :hash,
43
+ # Images: :hash,
44
+ Manufacturer: :hash,
45
+ Note: :hash,
46
+ ItemECommerce: :hash,
47
+ ItemShops: :hash,
48
+ ItemComponents: :hash,
49
+ ItemShelfLocations: :hash,
50
+ ItemVendorNums: :hash,
51
+ CustomFieldValues: :hash,
52
+ Prices: :hash,
53
+ Tags: :hash
54
+ )
15
55
 
16
- def self.id_field
17
- "itemID"
18
- end
56
+ relationships :ItemMatrix, :Category, :Images, DefaultVendor: :Vendor
19
57
 
20
- # FUNFACT: ItemMatrix data is returned during an `update` request,
21
- # but not during a `find` request.
22
- def item_matrix
23
- @ItemMatrix ||= owner.item_matrices.find(itemMatrixID) # rubocop:disable VariableName
58
+ def prices
59
+ @prices ||= Lightspeed::Prices.new(self.Prices)
24
60
  end
25
61
  end
26
62
  end
@@ -0,0 +1,13 @@
1
+ require_relative 'resource'
2
+
3
+ module Lightspeed
4
+ class ItemAttributeSet < Lightspeed::Resource
5
+ fields(
6
+ itemAttributeSetID: :id,
7
+ name: :string,
8
+ attributeName1: :string,
9
+ attributeName2: :string,
10
+ attributeName3: :string
11
+ )
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'collection'
2
+
3
+ require_relative 'item_attribute_set'
4
+
5
+ module Lightspeed
6
+ class ItemAttributeSets < Lightspeed::Collection
7
+ end
8
+ end
@@ -1,8 +1,9 @@
1
- require 'lightspeed/item_matrix'
2
- require 'lightspeed/account_resources'
1
+ require_relative 'collection'
2
+
3
+ require_relative 'item_matrix'
3
4
 
4
5
  module Lightspeed
5
- class ItemMatrices < AccountResources
6
+ class ItemMatrices < Lightspeed::Collection
6
7
  def self.resource_name
7
8
  "ItemMatrix"
8
9
  end
@@ -1,17 +1,55 @@
1
+ require_relative 'resource'
2
+
3
+ require_relative 'item_attribute_set'
4
+ require_relative 'category'
5
+ require_relative 'items'
6
+
1
7
  module Lightspeed
2
- class ItemMatrix < Lightspeed::Base
3
- attr_accessor :description, :tax, :defaultCost, :itemType, :modelYear, :archived,
4
- :timeStamp
8
+ class ItemMatrix < Lightspeed::Resource
9
+ fields(
10
+ itemMatrixID: :id,
11
+ description: :string,
12
+ tax: :boolean,
13
+ defaultCost: :decimal,
14
+ itemType: :string,
15
+ modelYear: :integer,
16
+ archived: :boolean,
17
+ timeStamp: :datetime,
18
+ itemAttributeSetID: :id,
19
+ manufacturerID: :id,
20
+ categoryID: :id,
21
+ defaultVendorID: :id,
22
+ taxClassID: :id,
23
+ seasonID: :id,
24
+ departmentID: :id,
25
+ itemECommerceID: :id,
26
+ itemAttributeSet: :hash,
27
+ Manufacturer: :hash,
28
+ Category: :hash,
29
+ TaxClass: :hash,
30
+ Season: :hash,
31
+ Department: :hash,
32
+ ItemECommerce: :hash,
33
+ Images: :hash,
34
+ Items: :hash,
35
+ CustomFieldValues: :hash,
36
+ Prices: :hash
37
+ )
38
+
39
+ relationships :ItemAttributeSet, :Category, :Items
5
40
 
6
- # Association keys
7
- attr_accessor :itemAttributeSetID, :manufacturerID, :categoryID, :defaultVendorID,
8
- :taxClassID, :seasonID, :departmentID, :itemECommerceID
41
+ # overrides
9
42
 
10
- # Embedded
11
- attr_accessor :Prices
43
+ def self.collection_name
44
+ 'ItemMatrices'
45
+ end
46
+
47
+ def singular_path_parent
48
+ account
49
+ end
12
50
 
13
- def self.id_field
14
- "itemMatrixID"
51
+ def prices
52
+ @prices ||= Lightspeed::Prices.new(self.Prices)
15
53
  end
16
54
  end
17
55
  end
@@ -1,12 +1,9 @@
1
- require 'lightspeed/item'
2
- require 'lightspeed/account_resources'
1
+ require_relative 'collection'
2
+
3
+ require_relative 'item'
3
4
 
4
5
  module Lightspeed
5
- class Items < Lightspeed::AccountResources
6
+ class Items < Lightspeed::Collection
6
7
  alias_method :archive, :destroy
7
-
8
- def self.resource_name
9
- "Item"
10
- end
11
8
  end
12
9
  end
@@ -0,0 +1,34 @@
1
+ require_relative 'resource'
2
+
3
+ module Lightspeed
4
+ class Order < Lightspeed::Resource
5
+ alias_method :archive, :destroy
6
+
7
+ fields(
8
+ orderID: :id,
9
+ orderedDate: :timestamp,
10
+ receivedDate: :timestamp,
11
+ arrivalDate: :timestamp,
12
+ shipInstructions: :integer,
13
+ stockInstructions: :integer,
14
+ shipCost: :decimal,
15
+ otherCost: :decimal,
16
+ complete: :boolean,
17
+ archived: :boolean,
18
+ discount: :boolean,
19
+ totalDiscount: :decimal,
20
+ totalQuantity: :decimal,
21
+ vendorID: :id,
22
+ noteID: :id,
23
+ shopID: :id,
24
+ Vendor: :hash,
25
+ Note: :hash,
26
+ Shop: :hash,
27
+ OrderLines: :hash,
28
+ CustomFieldValues: :hash,
29
+ timeStamp: :timestamp
30
+ )
31
+
32
+ end
33
+ end
34
+
@@ -0,0 +1,10 @@
1
+ require_relative 'collection'
2
+
3
+ require_relative 'order'
4
+
5
+ module Lightspeed
6
+ class Orders < Lightspeed::Collection
7
+ alias_method :archive, :destroy
8
+ end
9
+ end
10
+
@@ -0,0 +1,43 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'bigdecimal'
3
+
4
+ module Lightspeed
5
+ class Prices
6
+ def initialize(attributes)
7
+ @attributes = attributes
8
+ end
9
+
10
+ def prices
11
+ @prices ||= @attributes["ItemPrice"].map { |v| [v["useType"].parameterize.underscore.to_sym, BigDecimal.new(v["amount"])] }.to_h
12
+ end
13
+
14
+ def as_json
15
+ attributes
16
+ end
17
+ alias_method :to_h, :as_json
18
+
19
+ def to_json
20
+ as_json.to_json
21
+ end
22
+
23
+ def [](key)
24
+ prices[key]
25
+ end
26
+
27
+ def inspect
28
+ prices.inspect
29
+ end
30
+
31
+ def respond_to?(method, private_method)
32
+ prices.keys.include?(method) || super
33
+ end
34
+
35
+ def method_missing(method, *args, &block)
36
+ if prices.keys.include?(method)
37
+ prices[method]
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,55 +1,101 @@
1
+ require 'pp'
2
+ require 'net/http'
3
+
1
4
  module Lightspeed
2
5
  class Request
3
- attr_accessor :raw_request
6
+ attr_accessor :raw_request, :bucket_max, :bucket_level
7
+
8
+ SECONDS_TO_WAIT_WHEN_THROTTLED = 60 # API requirements.
4
9
 
5
- def self.base_url
6
- "https://api.merchantos.com/API"
10
+ class << self
11
+ attr_writer :verbose
7
12
  end
8
13
 
9
- def initialize(client, method:, path:, params: nil, body: nil)
10
- @raw_request = Typhoeus::Request.new(
11
- self.class.base_url + path,
12
- method: method,
13
- body: body,
14
- params: params
15
- )
16
-
17
- if client.oauth_token
18
- @raw_request.options[:headers].merge!(
19
- "Authorization" => "OAuth #{client.oauth_token}"
20
- )
21
- end
14
+ def self.verbose?
15
+ !! @verbose
16
+ end
22
17
 
23
- @raw_request.options[:userpwd] = "#{client.api_key}:apikey" if client.api_key
18
+ def self.base_host
19
+ "api.merchantos.com"
20
+ end
21
+
22
+ def self.base_path
23
+ "/API"
24
+ end
25
+
26
+ def initialize(client, method:, path:, params: nil, body: nil)
27
+ @method = method
28
+ @params = params
29
+ @path = path
30
+ @bucket_max = Float::INFINITY
31
+ @bucket_level = 0
32
+ @http = Net::HTTP.new(self.class.base_host, 443)
33
+ @http.use_ssl = true
34
+ @raw_request = request_class.new(uri)
35
+ @raw_request.body = body if body
36
+ @raw_request.set_form_data(@params) if @params && @method != :get
37
+ @raw_request["Authorization"] = "OAuth #{client.oauth_token}" if client.oauth_token
24
38
  end
25
39
 
26
40
  def perform
27
- response = raw_request.run
28
- if response.code == 200
41
+ response = @http.request(raw_request)
42
+ extract_rate_limits(response)
43
+ if response.code == "200"
29
44
  handle_success(response)
30
45
  else
31
46
  handle_error(response)
32
47
  end
48
+ rescue Lightspeed::Error::Throttled
49
+ retry_throttled_request
33
50
  end
34
51
 
35
52
  private
36
53
 
37
54
  def handle_success(response)
38
- JSON.parse(response.body)
55
+ json = JSON.parse(response.body)
56
+ pp json if self.class.verbose?
57
+ json
58
+ end
59
+
60
+ def retry_throttled_request
61
+ puts 'retrying throttled request after 60s.' if self.class.verbose?
62
+ sleep SECONDS_TO_WAIT_WHEN_THROTTLED
63
+ perform
39
64
  end
40
65
 
41
66
  def handle_error(response)
42
67
  data = JSON.parse(response.body)
43
- error = case response.code
44
- when 400
45
- Lightspeed::Errors::BadRequest
46
- when 401
47
- Lightspeed::Errors::Unauthorized
48
- when 500
49
- Lightspeed::Errors::InternalServerError
68
+ error = case response.code.to_s
69
+ when '400' then Lightspeed::Error::BadRequest
70
+ when '401' then Lightspeed::Error::Unauthorized
71
+ when '404' then Lightspeed::Error::NotFound
72
+ when '429' then Lightspeed::Error::Throttled
73
+ when /5../ then Lightspeed::Error::InternalServerError
74
+ else Lightspeed::Error
75
+ end
76
+ raise error, data["message"]
77
+ end
78
+
79
+ def extract_rate_limits(response)
80
+ if bucket_headers = response["X-LS-API-Bucket-Level"]
81
+ @bucket_level, @bucket_max = bucket_headers.split("/").map(&:to_f)
50
82
  end
83
+ end
51
84
 
52
- raise error.new(data["message"]) if error # rubocop:disable RaiseArgs
85
+ def uri
86
+ uri = self.class.base_path + @path
87
+ uri += "?" + URI.encode_www_form(@params) if @params && @method == :get
88
+ uri
53
89
  end
90
+
91
+ def request_class
92
+ case @method
93
+ when :get then Net::HTTP::Get
94
+ when :put then Net::HTTP::Put
95
+ when :post then Net::HTTP::Post
96
+ when :delete then Net::HTTP::Delete
97
+ end
98
+ end
99
+
54
100
  end
55
101
  end