vending-machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +155 -0
- data/Rakefile +14 -0
- data/lib/vend.rb +46 -0
- data/lib/vend/errors.rb +12 -0
- data/lib/vend/has_resources.rb +34 -0
- data/lib/vend/modules/declarative_setters.rb +74 -0
- data/lib/vend/modules/finders.rb +5 -0
- data/lib/vend/modules/paths.rb +37 -0
- data/lib/vend/remote_resource.rb +46 -0
- data/lib/vend/resource.rb +100 -0
- data/lib/vend/resource_collection.rb +72 -0
- data/lib/vend/resources/brand.rb +4 -0
- data/lib/vend/resources/contact.rb +4 -0
- data/lib/vend/resources/customer.rb +8 -0
- data/lib/vend/resources/inventory.rb +14 -0
- data/lib/vend/resources/outlet.rb +4 -0
- data/lib/vend/resources/payment_type.rb +4 -0
- data/lib/vend/resources/price_book_entry.rb +10 -0
- data/lib/vend/resources/product.rb +30 -0
- data/lib/vend/resources/product_type.rb +7 -0
- data/lib/vend/resources/register.rb +9 -0
- data/lib/vend/resources/register_sale_payment.rb +6 -0
- data/lib/vend/resources/register_sale_product.rb +12 -0
- data/lib/vend/resources/sale.rb +17 -0
- data/lib/vend/resources/supplier.rb +6 -0
- data/lib/vend/resources/tax.rb +13 -0
- data/lib/vend/resources/user.rb +4 -0
- data/lib/vend/response.rb +9 -0
- data/lib/vend/store.rb +103 -0
- data/lib/vend/version.rb +3 -0
- data/lib/vending-machine.rb +1 -0
- data/spec/resource_spec.rb +22 -0
- data/spec/resource_urls_spec.rb +59 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/store_spec.rb +21 -0
- data/spec/support/url_assertions.rb +23 -0
- data/spec/support/webmock.rb +53 -0
- data/vending-machine.gemspec +46 -0
- metadata +238 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Vend
|
2
|
+
class RemoteResource < Resource
|
3
|
+
include Vend::DeclarativeSetters
|
4
|
+
include Vend::Paths
|
5
|
+
|
6
|
+
def save
|
7
|
+
id ? save_existing : save_new
|
8
|
+
end
|
9
|
+
|
10
|
+
def load
|
11
|
+
self.attributes = store.get(
|
12
|
+
path: resource_path,
|
13
|
+
status: 200
|
14
|
+
).data
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def sendable_attributes
|
21
|
+
blacklist = %w{
|
22
|
+
created_at
|
23
|
+
updated_at
|
24
|
+
}
|
25
|
+
attributes.delete_if{|name,value| blacklist.include? name }.dup
|
26
|
+
end
|
27
|
+
|
28
|
+
def save_new
|
29
|
+
self.attributes = store.post(
|
30
|
+
path: create_path,
|
31
|
+
parameters: sendable_attributes,
|
32
|
+
status: 200
|
33
|
+
).data
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def save_existing
|
38
|
+
self.attributes = store.put(
|
39
|
+
path: update_path,
|
40
|
+
parameters: sendable_attributes,
|
41
|
+
status: 200
|
42
|
+
).data
|
43
|
+
self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Vend
|
4
|
+
class Resource
|
5
|
+
include Virtus
|
6
|
+
|
7
|
+
attr_accessor :store
|
8
|
+
|
9
|
+
attribute :id, String
|
10
|
+
|
11
|
+
alias_method :initialize_virtus, :initialize
|
12
|
+
def initialize attributes={}
|
13
|
+
if attributes[:store_object]
|
14
|
+
@store = store_object
|
15
|
+
end
|
16
|
+
initialize_virtus attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :set_attributes_virtus, :set_attributes
|
20
|
+
def set_attributes(attributes)
|
21
|
+
public_method_names = public_methods.map(&:to_s)
|
22
|
+
|
23
|
+
unknown_attributes = attributes.
|
24
|
+
dup.
|
25
|
+
delete_if{|k, v| public_method_names.include?("#{k.to_s}=") }
|
26
|
+
|
27
|
+
if unknown_attributes.size > 0
|
28
|
+
dates = %w{ created_at deleted_at updated_at }
|
29
|
+
self.extend Virtus unless self.respond_to?(:attribute)
|
30
|
+
unknown_attributes.
|
31
|
+
each do |k,v|
|
32
|
+
case
|
33
|
+
when dates.include?(k)
|
34
|
+
klass = DateTime
|
35
|
+
else
|
36
|
+
klass = String
|
37
|
+
end
|
38
|
+
self.attribute k, klass
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
set_attributes_virtus attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def has_many_reader(name, class_proc)
|
49
|
+
variable_name = "@_has_many_reader_#{name}"
|
50
|
+
instance_variable_get(variable_name) ||
|
51
|
+
instance_variable_set(variable_name, (
|
52
|
+
klass = class_proc.call
|
53
|
+
ResourceCollection.new(
|
54
|
+
store,
|
55
|
+
class_proc.call,
|
56
|
+
(@_has_many || {})[name] || nil
|
57
|
+
)
|
58
|
+
)
|
59
|
+
)
|
60
|
+
rescue KeyError
|
61
|
+
raise Error, "No reference to children :#{name}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_one_reader(name, class_proc)
|
65
|
+
klass = class_proc.call
|
66
|
+
record = (@_has_one || {}).fetch(name)
|
67
|
+
if record.is_a?(Resource)
|
68
|
+
record
|
69
|
+
else
|
70
|
+
@_has_one[name] = klass.new(record)
|
71
|
+
end
|
72
|
+
rescue KeyError
|
73
|
+
raise Error, "No reference to child :#{name}"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
module Attributes
|
78
|
+
class CSV < Virtus::Attribute::Object
|
79
|
+
primitive Array
|
80
|
+
|
81
|
+
def coerce(value)
|
82
|
+
require 'csv'
|
83
|
+
::CSV.parse(value).first
|
84
|
+
end
|
85
|
+
end
|
86
|
+
class Boolean < Virtus::Attribute::Object
|
87
|
+
primitive Boolean
|
88
|
+
|
89
|
+
def coerce(value)
|
90
|
+
if [1, '1'].include? value
|
91
|
+
true
|
92
|
+
elsif [0, '0'].include? value
|
93
|
+
false
|
94
|
+
else
|
95
|
+
value == true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Vend
|
2
|
+
class ResourceCollection
|
3
|
+
attr_accessor :parameters, :store
|
4
|
+
|
5
|
+
include Vend::Finders
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(store, klass, records = nil)
|
9
|
+
@store = store
|
10
|
+
@klass = klass
|
11
|
+
@records = records
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Check for a collection method
|
16
|
+
# defined on the resource
|
17
|
+
def method_missing symbol, *args
|
18
|
+
if @klass.collection_methods[symbol]
|
19
|
+
instance_exec *args, &@klass.collection_methods[symbol]
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(attributes = {})
|
26
|
+
build(attributes).tap(&:save)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build(attributes)
|
30
|
+
@klass.new(attributes.merge store: @store)
|
31
|
+
end
|
32
|
+
|
33
|
+
def << record
|
34
|
+
(@records || []) << record
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_a
|
38
|
+
records.to_a
|
39
|
+
end
|
40
|
+
|
41
|
+
def each
|
42
|
+
records.each do |attributes|
|
43
|
+
yield build(attributes)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def http_response
|
50
|
+
@_http_response ||= @store.get(
|
51
|
+
path: @klass.collection_path,
|
52
|
+
parameters: @parameters,
|
53
|
+
status: 200
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def remote_records
|
58
|
+
http_response.data.first.last
|
59
|
+
end
|
60
|
+
|
61
|
+
def records
|
62
|
+
# Data comes back with a root element, so get the first response's value
|
63
|
+
# { products: [ {}, {}, ..] }.first.last == [ {}, {}, ... ]
|
64
|
+
@records ||= remote_records
|
65
|
+
end
|
66
|
+
|
67
|
+
def clear
|
68
|
+
@records = nil
|
69
|
+
@_http_response = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# TODO: Implement the attribute declaration as a Proc to avoid this
|
2
|
+
module Vend
|
3
|
+
class Product < RemoteResource
|
4
|
+
end
|
5
|
+
end
|
6
|
+
module Vend
|
7
|
+
class Inventory < RemoteResource
|
8
|
+
attribute :attributed_cost, Float
|
9
|
+
attribute :count, Integer
|
10
|
+
attribute :product, Vend::Product
|
11
|
+
attribute :outlet, Vend::Outlet
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Vend
|
2
|
+
class Product < RemoteResource
|
3
|
+
##
|
4
|
+
# Attribute Coercion
|
5
|
+
attribute :active, Boolean
|
6
|
+
attribute :brand, Vend::Brand
|
7
|
+
attribute :display_retail_price_tax_inclusive, Attributes::Boolean
|
8
|
+
attribute :inventory, Array[Vend::Inventory]
|
9
|
+
attribute :price, Float
|
10
|
+
attribute :price_book_entries, Array[Vend::PriceBookEntry]
|
11
|
+
attribute :product_type, Vend::ProductType
|
12
|
+
attribute :supplier, Vend::Supplier
|
13
|
+
attribute :supply_price, Float
|
14
|
+
attribute :tags, Attributes::CSV
|
15
|
+
attribute :tax, Float
|
16
|
+
attribute :tax_rate, Float
|
17
|
+
|
18
|
+
##
|
19
|
+
# Scopes
|
20
|
+
collection_scope :active, default: true
|
21
|
+
collection_scope :order_by
|
22
|
+
collection_scope :order_direction
|
23
|
+
|
24
|
+
collection_method :order do |by, direction='ASC'|
|
25
|
+
order_by by
|
26
|
+
order_direction direction
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Vend
|
2
|
+
class Register < RemoteResource
|
3
|
+
attribute :invoice_sequence, Integer
|
4
|
+
attribute :print_receipt, Attributes::Boolean
|
5
|
+
attribute :register_close_time, DateTime
|
6
|
+
attribute :register_open_time, DateTime
|
7
|
+
attribute :register_open_count_sequence, Integer
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Vend
|
2
|
+
class RegisterSaleProduct < Resource
|
3
|
+
attribute :display_retail_price_tax_inclusive, Attributes::Boolean
|
4
|
+
attribute :price, Float
|
5
|
+
attribute :price_set, Boolean
|
6
|
+
attribute :price_total, Float
|
7
|
+
attribute :quantity, Integer
|
8
|
+
attribute :tax, Float
|
9
|
+
attribute :tax_rate, Float
|
10
|
+
attribute :tax_total, Float
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Vend
|
2
|
+
class Sale < RemoteResource
|
3
|
+
attribute :customer, Vend::Customer
|
4
|
+
attribute :invoice_number, Integer
|
5
|
+
attribute :register_sale_products, Array[RegisterSaleProduct]
|
6
|
+
attribute :register_sale_payments, Array[RegisterSalePayment]
|
7
|
+
attribute :total_cost, Float
|
8
|
+
attribute :total_price, Float
|
9
|
+
attribute :total_tax, Float
|
10
|
+
attribute :totals, Hash[Symbol => Float] # Requires updated virtus gem
|
11
|
+
#attribute :totals, Array[Float]
|
12
|
+
attribute :user, Vend::User
|
13
|
+
|
14
|
+
path "register_sale"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
data/lib/vend/store.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require "addressable/uri"
|
2
|
+
require "faraday"
|
3
|
+
require "faraday_middleware"
|
4
|
+
|
5
|
+
module Vend
|
6
|
+
class Store
|
7
|
+
include HasResources
|
8
|
+
|
9
|
+
def initialize name, username, password
|
10
|
+
@name, @credentials = name, [ username, password ]
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Dispatch a request using HTTP GET
|
15
|
+
def get options
|
16
|
+
dispatch :get, options
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Dispatch a request using HTTP POST
|
21
|
+
def post options
|
22
|
+
dispatch :post, options
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
##
|
28
|
+
# Dispatches an HTTP request
|
29
|
+
def dispatch method, options
|
30
|
+
path = options[:path]
|
31
|
+
parameters = options[:parameters]
|
32
|
+
data = options[:data]
|
33
|
+
|
34
|
+
if parameters
|
35
|
+
path = Addressable::URI.parse(path).tap do |uri|
|
36
|
+
uri.query_values = (uri.query_values || {}).merge(parameters)
|
37
|
+
end.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
headers = faraday.headers.merge(options[:headers] || {})
|
41
|
+
response = faraday.run_request(method, path, data, headers)
|
42
|
+
|
43
|
+
assert_http_status(response, options[:status])
|
44
|
+
|
45
|
+
::Vend::Response.new(
|
46
|
+
response.status,
|
47
|
+
response.headers,
|
48
|
+
response.body
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Raises an error unless the response status matches the required status
|
54
|
+
def assert_http_status(response, status)
|
55
|
+
case response.status
|
56
|
+
when nil, status then return
|
57
|
+
when 401
|
58
|
+
raise Unauthorized
|
59
|
+
when 422
|
60
|
+
raise ValidationFailed, "Validation failed: #{response.body.to_s}"
|
61
|
+
when 500
|
62
|
+
raise ServerError, internal_server_error_message(status, response)
|
63
|
+
else
|
64
|
+
raise Error, "Expected HTTP #{status}, got HTTP #{response.status}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def internal_server_error_message(expected_status, response)
|
69
|
+
body = response.body
|
70
|
+
if body["type"] && body["message"] && body["backtrace"]
|
71
|
+
"Expected HTTP %d, got HTTP %d with error:\n%s\n%s\n\n%s" % [
|
72
|
+
expected_status,
|
73
|
+
response.status,
|
74
|
+
response.body["type"],
|
75
|
+
response.body["message"],
|
76
|
+
response.body["backtrace"].join("\n"),
|
77
|
+
]
|
78
|
+
else
|
79
|
+
"Expected HTTP %d, got %d" % [
|
80
|
+
expected_status,
|
81
|
+
response.status,
|
82
|
+
]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Base URL for the Vend AP
|
88
|
+
def url
|
89
|
+
"%s://%s.%s/api" % [ Vend.scheme, @name, Vend.domain ]
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Memoized Faraday client with auth credentials
|
94
|
+
def faraday
|
95
|
+
@_faraday ||= Faraday.new(url) do |conn|
|
96
|
+
conn.request :json
|
97
|
+
conn.response :json
|
98
|
+
conn.basic_auth *@credentials
|
99
|
+
conn.adapter Faraday.default_adapter
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|