lightspeed_pos 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f479289d48eb83582ea0f30e93bf82644e0a5cad
4
- data.tar.gz: a0b8758eaeeaa5f7f6f044cc9bdd3689156f6d03
3
+ metadata.gz: d3960f7c39629798ffae0406f84fb2cbcf897390
4
+ data.tar.gz: f900f23a42324bf61268b0ab4f43ac0528971c38
5
5
  SHA512:
6
- metadata.gz: be2f7ba8a83b024b43def375f084ab63f695087651563e690f4ec235c453b1f19d54cf0e938f5fd31a04fb83e110d8c0fde4c5006bac7c405f6da6c987bc9891
7
- data.tar.gz: e04d5d7c4dabf225688e79e5bf071cd86c371ec5dd2fffea4d88721970ceeb4558cf3b996a24b42ebaf3a3f9be15cd2d72ee3b5dec07011bfc38a4d37bbeaa41
6
+ metadata.gz: 7e7e1f10edaa984d5e232c9706f32e3b6d13bb8cae0cb3c5e00f773335493a3372085e7d85c7411ccb425f20fe8ab341d98075273d39f5e418d839b32cf99478
7
+ data.tar.gz: 856a6628c9c7c4dd36b10e25a253bc394230600e5632c3e961242a285e9154d34b99564fb063e06ebf376feb7ffd74ddd7397faaa9a2e20a08206f4e7a02e474
data/README.markdown CHANGED
@@ -1,23 +1,13 @@
1
1
  # Lightspeed POS
2
2
 
3
- [![Build Status](https://travis-ci.org/radar/lightspeed-pos.svg?branch=master)](https://travis-ci.org/radar/lightspeed-pos)
4
- [![Code Climate](https://codeclimate.com/github/radar/lightspeed-pos/badges/gpa.svg)](https://codeclimate.com/github/radar/lightspeed-pos)
3
+ An _unofficial_ gem for interacting with [Lightspeed's Point of Sale API](http://www.lightspeedpos.com/retail/help/developers/api/basics/), ([documentation](http://cloud-docs.lightspeedapp.com/API/APIHelp.help)).
5
4
 
6
-
7
- An _unofficial_ gem for interacting with [Lightspeed's Point of Sale API](http://www.lightspeedpos.com/retail/help/developers/api/basics/). Works with API keys for the time being.
8
-
9
- Most definitely not production ready yet, but you can help by submitting pull requests!
5
+ Not all endpoints are implemented yet, but you can help by submitting pull requests!
10
6
 
11
7
  ## Getting Started
12
8
 
13
9
  First, intialize a new client:
14
10
 
15
- ```ruby
16
- client = Lightspeed::Client.new(api_key: "YOUR_API_KEY_HERE")
17
- ```
18
-
19
- **OR** you may also choose to pass through an OAuth access token if you have one:
20
-
21
11
  ```ruby
22
12
  client = Lightspeed::Client.new(oauth_token: "YOUR_ACCESS_TOKEN_HERE")
23
13
  ```
@@ -25,22 +15,33 @@ client = Lightspeed::Client.new(oauth_token: "YOUR_ACCESS_TOKEN_HERE")
25
15
  Next, make a request for your accounts:
26
16
 
27
17
  ```ruby
28
- accounts = client.accounts
18
+ accounts = client.accounts.all
29
19
  ```
30
20
 
31
21
  Pick the account you want to use, and then start using it:
32
22
 
33
23
  ```ruby
34
24
  account = accounts.first
35
- account.items # This will return the first 100 items from the account
25
+ account.items.first
36
26
  ```
37
27
 
38
- ## Account Resources
28
+ ## Resources
39
29
 
40
- Account resources share a common API. Account resources that are currently supported by this library are:
30
+ resources share a common API. Resources that are currently supported by this library are:
41
31
 
32
+ * Accounts
42
33
  * Categories
34
+ * Employees
43
35
  * Items
36
+ * Item Matrices
37
+ * Item Attribute Sets
38
+ * Images
39
+ * Inventories
40
+ * Orders
41
+ * Sales
42
+ * Shops
43
+ * Special Orders
44
+ * Vendors
44
45
 
45
46
  To work with account resources, you first need to fetch an account. The examples below are for items, but will also work with other types listed above.
46
47
 
@@ -52,6 +53,28 @@ You can fetch a list of items with this:
52
53
  account.items.all
53
54
  ```
54
55
 
56
+ You can pass query parameters to this by using the `params` keyword:
57
+
58
+ ```ruby
59
+ account.items.all(params: { itemMatrixID: 0 })
60
+ ```
61
+
62
+ You can enumerate over a group of 100 resources at a time (the max in a single request) using `each_page`
63
+
64
+ ```ruby
65
+ account.items.each_page! do |items|
66
+ # ItemImporter.import(items)
67
+ end
68
+ ```
69
+
70
+ Or enumerate over each resource using #each (this still only does a request for each 100 items)
71
+
72
+ ```ruby
73
+ account.items.each do |item|
74
+ # ItemImporter.import(item)
75
+ end
76
+ ```
77
+
55
78
  ### Show
56
79
 
57
80
  You can fetch a particular item by its ID by doing this:
@@ -59,15 +82,19 @@ You can fetch a particular item by its ID by doing this:
59
82
  ```ruby
60
83
  account.items.find(1)
61
84
  ```
85
+ If item with id of `1` is not there, this will raise `Lightspeed::Error::NotFound`
86
+
87
+ You can fetch the first item using `first`
88
+ ```ruby
89
+ account.items.first
90
+ ```
62
91
 
63
92
  ### Create
64
93
 
65
94
  You can create a particular item by calling `create`:
66
95
 
67
96
  ```ruby
68
- account.items.create({
69
- description: "Onesie"
70
- })
97
+ account.items.create(description: "Onesie")
71
98
  ```
72
99
 
73
100
  ### Update
@@ -75,24 +102,25 @@ account.items.create({
75
102
  You can update a particular item by calling `update`, passing that item's ID and providing a list of attributes to update:
76
103
 
77
104
  ```ruby
78
- account.items.update(1, {
79
- description: "Onesie"
80
- })
105
+ account.items.update(1, description: "Onesie")
106
+ # OR
107
+ account.items.find(1)
108
+ item.update(description: "Onesie")
81
109
  ```
82
110
 
83
- This method isn't available on items themselves because I couldn't work out how to share the account ID easily there.
84
-
85
111
  ### Destroy
86
112
 
87
113
  You can destroy a particular item by calling `destroy` and passing that item's ID:
88
114
 
89
115
  ```ruby
90
- account.items.destroy(1)
116
+ account.images.destroy(1)
117
+ # OR
118
+ account.images.find(1)
119
+ item.destroy
91
120
  ```
92
121
 
93
- For the `Items` resource, this is aliased to `archive`:
122
+ For the `Item` resource, `destroy` is aliased to `archive`:
94
123
 
95
- ```ruby
96
- account.items.archive(1)
97
- ```
124
+ ## Rate Limiting
98
125
 
126
+ This gem respects the `X-LS-API-Bucket-Level` header by pausing before a request that would otherwise overrun the API rate limit. It calls `Kernel.sleep` with the minimum number of seconds needed to avoid hitting the limit, which suspends the current thread during that time. If you need to make API calls across multiple Lightspeed accounts using this gem, its recommended to make requests against each account in a separate thread.
data/bin/console CHANGED
@@ -1,14 +1,22 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "lightspeed/pos"
3
+ require 'bundler/setup'
4
+ require 'dotenv'
5
+ require 'lightspeed_pos'
6
+ Lightspeed::Request.verbose = true
5
7
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
8
  # require "pry"
11
9
  # Pry.start
10
+ Dotenv.load
11
+ def client
12
+ token = ENV['LIGHTSPEED_OAUTH_TOKEN']
13
+ raise 'set LIGHTSPEED_OAUTH_TOKEN as an envorinment variable to use this' unless token
14
+ @client ||= Lightspeed::Client.new(oauth_token: token)
15
+ end
16
+
17
+ def account
18
+ client.accounts.first
19
+ end
12
20
 
13
- require "irb"
21
+ require 'irb'
14
22
  IRB.start
@@ -1,48 +1,49 @@
1
- require 'lightspeed/base'
2
- require 'lightspeed/categories'
3
- require 'lightspeed/items'
4
- require 'lightspeed/item_matrices'
1
+ require 'uri'
2
+ require_relative 'categories'
3
+ require_relative 'employees'
4
+ require_relative 'items'
5
+ require_relative 'item_matrices'
6
+ require_relative 'item_attribute_sets'
7
+ require_relative 'images'
8
+ require_relative 'inventories'
9
+ require_relative 'orders'
10
+ require_relative 'sales'
11
+ require_relative 'shops'
12
+ require_relative 'special_orders'
13
+ require_relative 'vendors'
5
14
 
6
15
  module Lightspeed
7
- class Account < Lightspeed::Base
8
- attr_accessor :id, :name, :link
9
-
10
- def client
11
- owner
12
- end
13
-
14
- def items
15
- item_proxy
16
- end
17
-
18
- def categories
19
- category_proxy
20
- end
21
-
22
- def item_matrices
23
- item_matrices_proxy
24
- end
25
-
26
- def instantiate(*args)
27
- client.instantiate(self, *args)
28
- end
29
-
30
- private
31
-
32
- def self.id_field
33
- "accountID"
34
- end
35
-
36
- def item_proxy
37
- @item_proxy ||= Lightspeed::Items.new(self)
38
- end
39
-
40
- def category_proxy
41
- @category_proxy ||= Lightspeed::Categories.new(self)
42
- end
43
-
44
- def item_matrices_proxy
45
- @item_matrices_proxy ||= Lightspeed::ItemMatrices.new(self)
16
+ class Account < Lightspeed::Resource
17
+ fields(
18
+ accountID: :id,
19
+ name: :string,
20
+ link: :hash
21
+ )
22
+ relationships(
23
+ :Categories,
24
+ :Employees,
25
+ :Images,
26
+ :Inventories,
27
+ :ItemMatrices,
28
+ :ItemAttributeSets,
29
+ :Items,
30
+ :Orders,
31
+ :Sales,
32
+ :Shops,
33
+ :SpecialOrders,
34
+ :Vendors
35
+ )
36
+
37
+ def account
38
+ self
39
+ end
40
+
41
+ def link
42
+ if @link.is_a?(Hash)
43
+ @link['@attributes']['href']
44
+ else
45
+ @link
46
+ end
46
47
  end
47
48
  end
48
49
  end
@@ -0,0 +1,22 @@
1
+ require_relative 'collection'
2
+ require_relative 'account'
3
+
4
+ module Lightspeed
5
+ class Accounts < Lightspeed::Collection
6
+ def base_path
7
+ '/Account'
8
+ end
9
+
10
+ def account
11
+ first_loaded || first
12
+ end
13
+
14
+ def page(n, *args)
15
+ # turns out lightspeed doesn't respect pagination for accounts.
16
+ # so page(1) is identical to page(0).
17
+ # they should be different, thus.
18
+ # if someone has more than 100 store accounts, well, good for them.
19
+ n.zero? ? super : []
20
+ end
21
+ end
22
+ end
@@ -1,10 +1,10 @@
1
- require 'lightspeed/category'
2
- require 'lightspeed/account_resources'
1
+ require_relative 'category'
2
+ require_relative 'collection'
3
3
 
4
4
  module Lightspeed
5
- class Categories < Lightspeed::AccountResources
6
- def self.resource_name
7
- "Category"
5
+ class Categories < Lightspeed::Collection
6
+ def load_relations_default
7
+ nil
8
8
  end
9
9
  end
10
10
  end
@@ -1,10 +1,19 @@
1
- module Lightspeed
2
- class Category < Base
3
- attr_accessor :name, :nodeDepth, :fullPathName, :leftNode, :rightNode, :timeStamp, :parentID,
4
- :createTime
1
+ require_relative 'resource'
5
2
 
6
- def self.id_field
7
- "categoryID"
8
- end
3
+ module Lightspeed
4
+ class Category < Lightspeed::Resource
5
+ fields(
6
+ categoryID: :id,
7
+ name: :string,
8
+ nodeDepth: :integer,
9
+ fullPathName: :string,
10
+ leftNode: :integer,
11
+ rightNode: :integer,
12
+ createTime: :datetime,
13
+ timeStamp: :datetime,
14
+ parentID: :id,
15
+ Category: :hash
16
+ )
17
+ relationships Parent: :Category
9
18
  end
10
19
  end
@@ -1,45 +1,56 @@
1
- require 'lightspeed/account'
2
- require 'lightspeed/request'
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ require_relative 'accounts'
4
+ require_relative 'request'
5
+ require_relative 'request_throttler'
3
6
 
4
7
  module Lightspeed
5
8
  class Client
6
- attr_accessor :api_key, :oauth_token
9
+ attr_accessor :oauth_token, :throttler
7
10
 
8
- def initialize(api_key: nil, oauth_token: nil)
9
- @api_key = api_key
11
+ def initialize(oauth_token: nil)
10
12
  @oauth_token = oauth_token
11
- end
12
-
13
- def request(**args)
14
- Lightspeed::Request.new(self, **args)
13
+ @throttler = Lightspeed::RequestThrottler.new
15
14
  end
16
15
 
17
16
  # Returns a list of accounts that you have access to.
18
17
  def accounts
19
- request = request(method: :get, path: "/Account.json")
20
- response = request.perform
21
- instantiate(response["Account"], Lightspeed::Account)
18
+ @accounts ||= Lightspeed::Accounts.new(context: self)
22
19
  end
23
20
 
24
- # Instantiates a bunch of records from Lightspeed into their proper classes.
25
- def instantiate(owner = self, records, klass)
26
- records = splat(records)
27
- records.map do |record|
28
- klass.new(owner, record)
21
+ def load_json(json)
22
+ data = JSON.parse(json)
23
+ Array.wrap(data).map do |resource|
24
+ resource = resource_class.new(context: self, attributes: resource)
25
+ @resources[resource.id] = resource
29
26
  end
30
27
  end
31
28
 
29
+ def get(**args)
30
+ perform_request(args.merge(method: :get))
31
+ end
32
+
33
+ def post(**args)
34
+ perform_request(args.merge(method: :post))
35
+ end
36
+
37
+ def put(**args)
38
+ perform_request(args.merge(method: :put))
39
+ end
40
+
41
+ def delete(**args)
42
+ perform_request(args.merge(method: :delete))
43
+ end
44
+
32
45
  private
33
46
 
34
- # Converts a thing to an Array unless it is already.
35
- # Unfortunately necessary because Lightspeed's API may return an object,
36
- # or an array of objects.
37
- #
38
- # The compact is becuase it may return nothing at all.
39
- # In the example of fetching categories resource where there are no categories,
40
- # response["Category"] will not be present.
41
- def splat(thing)
42
- (thing.is_a?(Array) ? thing : [thing]).compact
47
+ def perform_request(**args)
48
+ @throttler.perform_request request(**args)
43
49
  end
50
+
51
+ def request **args
52
+ Lightspeed::Request.new(self, **args)
53
+ end
54
+
44
55
  end
45
56
  end
@@ -0,0 +1,218 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/core_ext/array/wrap'
3
+ require 'active_support/json'
4
+ require 'active_support/core_ext/object/json'
5
+
6
+ module Lightspeed
7
+ class Collection
8
+ PER_PAGE = 100 # the max page of records returned in a request
9
+
10
+ attr_accessor :context, :resources
11
+
12
+ def initialize(context:, attributes: nil)
13
+ self.context = context
14
+ instantiate(attributes)
15
+ end
16
+
17
+ def account
18
+ context.account
19
+ end
20
+
21
+ def unload
22
+ @resources = {}
23
+ end
24
+
25
+ def load_json(json)
26
+ instantiate(JSON.parse(json))
27
+ end
28
+
29
+ def client
30
+ return context if context.is_a?(Lightspeed::Client)
31
+ account.client
32
+ end
33
+
34
+ def first(params: {})
35
+ params = params.merge(limit: 1)
36
+ instantiate(get(params: params)).first
37
+ end
38
+
39
+ def size(params: {})
40
+ params = params.merge(limit: 1, load_relations: nil)
41
+ get(params: params)['@attributes']['count'].to_i
42
+ end
43
+ alias_method :length, :size
44
+
45
+ def each_loaded
46
+ @resources ||= {}
47
+ @resources.each_value
48
+ end
49
+
50
+ def all_loaded
51
+ each_loaded.to_a
52
+ end
53
+
54
+ def first_loaded
55
+ each_loaded.first
56
+ end
57
+
58
+ def size_loaded
59
+ @resources.size
60
+ end
61
+
62
+ def all(params: {})
63
+ enum_page(params: params).to_a.flatten(1)
64
+ end
65
+
66
+ def each_page(per_page: PER_PAGE, params: {}, &block)
67
+ enum_page(per_page: per_page, params: params).each(&block)
68
+ end
69
+
70
+ def enum_page(per_page: PER_PAGE, params: {})
71
+ Enumerator.new do |yielder|
72
+ loop.with_index do |_, n|
73
+ resources = page(n, per_page: per_page, params: params)
74
+ yielder << resources
75
+ raise StopIteration if resources.length < per_page
76
+ end
77
+ end
78
+ end
79
+
80
+ def enum(per_page: PER_PAGE, params: {})
81
+ Enumerator.new do |yielder|
82
+ each_page(per_page: per_page, params: params) do |page|
83
+ page.each { |resource| yielder << resource }
84
+ end
85
+ end
86
+ end
87
+
88
+ def each(per_page: PER_PAGE, params: {}, &block)
89
+ enum(per_page: per_page, params: params).each(&block)
90
+ end
91
+
92
+ def find(id)
93
+ first(params: { resource_class.id_field => id }) || handle_not_found(id)
94
+ end
95
+
96
+ def create(attributes = {})
97
+ instantiate(post(body: attributes.to_json)).first
98
+ end
99
+
100
+ def update(id, attributes = {})
101
+ instantiate(put(id, body: attributes.to_json)).first
102
+ end
103
+
104
+ def destroy(id)
105
+ instantiate(delete(id)).first
106
+ end
107
+
108
+ def self.collection_name
109
+ name.demodulize
110
+ end
111
+
112
+ def self.resource_name
113
+ collection_name.singularize
114
+ end
115
+
116
+ def self.resource_class
117
+ "Lightspeed::#{resource_name}".constantize
118
+ end
119
+
120
+ def base_path
121
+ "#{account.base_path}/#{resource_name}"
122
+ end
123
+
124
+ def inspect
125
+ "#<#{self.class.name} API#{base_path}>"
126
+ end
127
+
128
+ def as_json(*args)
129
+ return if all_loaded.empty?
130
+ { resource_name => all_loaded.as_json(*args) }
131
+ end
132
+ alias_method :to_h, :as_json
133
+
134
+ def to_json(*args)
135
+ as_json.to_json(*args)
136
+ end
137
+
138
+ def page(n, per_page: PER_PAGE, params: {})
139
+ params = params.merge(limit: per_page, offset: per_page * n)
140
+ instantiate(get(params: params))
141
+ end
142
+
143
+ def load_relations_default
144
+ 'all'
145
+ end
146
+
147
+ private
148
+
149
+ def handle_not_found(id)
150
+ raise Lightspeed::Error::NotFound, "Could not find a #{resource_name} with #{resource_class.id_field}=#{id}"
151
+ end
152
+
153
+ def context_params
154
+ if context.class.respond_to?(:id_field) &&
155
+ resource_class.method_defined?(context.class.id_field.to_sym)
156
+ { context.class.id_field => context.id }
157
+ else
158
+ {}
159
+ end
160
+ end
161
+
162
+ def instantiate(response)
163
+ return [] unless response.is_a?(Hash)
164
+ @resources ||= {}
165
+ Array.wrap(response[resource_name]).map do |resource|
166
+ resource = resource_class.new(context: self, attributes: resource)
167
+ @resources[resource.id] = resource
168
+ end
169
+ end
170
+
171
+ def resource_class
172
+ self.class.resource_class
173
+ end
174
+
175
+ def resource_name
176
+ self.class.resource_name
177
+ end
178
+
179
+ def get(params: {})
180
+ params = { load_relations: load_relations_default }
181
+ .merge(context_params)
182
+ .merge(params)
183
+ .reject { |_, v| v.nil? }
184
+ client.get(
185
+ path: collection_path,
186
+ params: params
187
+ )
188
+ end
189
+
190
+ def post(body:)
191
+ client.post(
192
+ path: collection_path,
193
+ body: body
194
+ )
195
+ end
196
+
197
+ def put(id, body:)
198
+ client.put(
199
+ path: resource_path(id),
200
+ body: body
201
+ )
202
+ end
203
+
204
+ def delete(id)
205
+ client.delete(
206
+ path: resource_path(id)
207
+ )
208
+ end
209
+
210
+ def collection_path
211
+ "#{base_path}.json"
212
+ end
213
+
214
+ def resource_path(id)
215
+ "#{base_path}/#{id}.json"
216
+ end
217
+ end
218
+ end