cornerstore 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +75 -0
- data/Rakefile +1 -0
- data/cornerstore.gemspec +23 -0
- data/examples/cornerstore.yml +11 -0
- data/lib/cornerstore.rb +84 -0
- data/lib/cornerstore/api.rb +18 -0
- data/lib/cornerstore/api/address.rb +12 -0
- data/lib/cornerstore/api/cancellation.rb +22 -0
- data/lib/cornerstore/api/carrier.rb +3 -0
- data/lib/cornerstore/api/cart.rb +49 -0
- data/lib/cornerstore/api/collection.rb +25 -0
- data/lib/cornerstore/api/customer.rb +4 -0
- data/lib/cornerstore/api/differentiating_property.rb +15 -0
- data/lib/cornerstore/api/image.rb +20 -0
- data/lib/cornerstore/api/line_item.rb +74 -0
- data/lib/cornerstore/api/order.rb +94 -0
- data/lib/cornerstore/api/payment_means.rb +32 -0
- data/lib/cornerstore/api/price.rb +77 -0
- data/lib/cornerstore/api/product.rb +72 -0
- data/lib/cornerstore/api/property.rb +30 -0
- data/lib/cornerstore/api/search.rb +35 -0
- data/lib/cornerstore/api/shipment.rb +24 -0
- data/lib/cornerstore/api/variant.rb +49 -0
- data/lib/cornerstore/model.rb +117 -0
- data/lib/cornerstore/resource.rb +8 -0
- data/lib/cornerstore/resource/base.rb +83 -0
- data/lib/cornerstore/resource/filter.rb +21 -0
- data/lib/cornerstore/resource/remote.rb +27 -0
- data/lib/cornerstore/resource/writable.rb +20 -0
- data/lib/cornerstore/version.rb +3 -0
- data/spec/order_spec.rb +78 -0
- data/spec/price_spec.rb +51 -0
- data/spec/spec_helper.rb +22 -0
- metadata +126 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
class Cornerstore::Order < Cornerstore::Model::Base
|
2
|
+
attr_accessor :number,
|
3
|
+
:reference,
|
4
|
+
:checkout_id,
|
5
|
+
:placed_at,
|
6
|
+
:invoice_number,
|
7
|
+
:invoiced_at,
|
8
|
+
:checkout_started_at,
|
9
|
+
:requested_carrier,
|
10
|
+
:weight,
|
11
|
+
:subtotal,
|
12
|
+
:shipping_costs,
|
13
|
+
:payment_costs,
|
14
|
+
:sales_tax,
|
15
|
+
:total,
|
16
|
+
:customer_comment,
|
17
|
+
|
18
|
+
:customer,
|
19
|
+
:shipping_address,
|
20
|
+
:billing_address,
|
21
|
+
:payment_means,
|
22
|
+
:requested_carrier,
|
23
|
+
:line_items,
|
24
|
+
:shipments,
|
25
|
+
:cancellations
|
26
|
+
|
27
|
+
def initialize(attributes, parent=nil)
|
28
|
+
self.payment_costs = Cornerstore::Price.new(attributes.delete('payment_costs')) if attributes['payment_costs']
|
29
|
+
self.shipping_costs = Cornerstore::Price.new(attributes.delete('shipping_costs')) if attributes['shipping_costs']
|
30
|
+
self.subtotal = Cornerstore::Price.new(attributes.delete('subtotal'))
|
31
|
+
self.sales_tax = Cornerstore::Price.new(attributes.delete('sales_tax')) if attributes['sales_tax']
|
32
|
+
self.total = Cornerstore::Price.new(attributes.delete('total'))
|
33
|
+
|
34
|
+
self.customer = Cornerstore::Customer.new(attributes.delete('customer'))
|
35
|
+
self.shipping_address = Cornerstore::Address.new(attributes.delete('shipping_address'))
|
36
|
+
self.billing_address = Cornerstore::Address.new(attributes.delete('billing_address'))
|
37
|
+
self.payment_means = Cornerstore::PaymentMeans.new(attributes.delete('payment_means'))
|
38
|
+
self.requested_carrier = Cornerstore::Carrier.new(attributes.delete('requested_carrier'))
|
39
|
+
|
40
|
+
self.line_items = Cornerstore::LineItem::Resource.new(self, attributes.delete('line_items') || [], 'line_items')
|
41
|
+
self.shipments = Cornerstore::Shipment::Resource.new(self, attributes.delete('shipments') || [], 'shipments')
|
42
|
+
self.cancellations = Cornerstore::Cancellation::Resource.new(self, attributes.delete('cancellations') || [], 'cancellations')
|
43
|
+
|
44
|
+
self.placed_at = DateTime.parse(attributes.delete('placed_at')) unless attributes['placed_at'].blank?
|
45
|
+
self.invoiced_at = DateTime.parse(attributes.delete('invoiced_at')) unless attributes['invoiced_at'].blank?
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def requires_shipment?
|
51
|
+
self.line_items.any? { |li| li.requires_shipment? }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if at least one line item of this order is shipped
|
55
|
+
def shipped?
|
56
|
+
!self.shipments.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if at least one line item of this order is canceled
|
60
|
+
def canceled?
|
61
|
+
!self.cancellations.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def invoiced?
|
65
|
+
!self.invoice_number.blank?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns true if all line items are shipped
|
69
|
+
def completely_shipped?
|
70
|
+
self.shipments.collect(&:shipped_items).flatten.size == self.line_items.size
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns true if all line items are canceled
|
74
|
+
def completely_canceled?
|
75
|
+
self.cancellations.collect(&:canceled_items).flatten.size == self.line_items.size
|
76
|
+
end
|
77
|
+
|
78
|
+
def handling_costs
|
79
|
+
if self.shipping_costs and self.payment_costs
|
80
|
+
self.shipping_costs + self.payment_costs
|
81
|
+
elsif self.shipping_costs
|
82
|
+
self.shipping_costs
|
83
|
+
elsif self.payment_costs
|
84
|
+
self.payment_costs
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_signature
|
91
|
+
# TODO: Implement signature check
|
92
|
+
return true
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Cornerstore::PaymentMeans < Cornerstore::Model::Base
|
2
|
+
attr_accessor :kind,
|
3
|
+
:paid_at,
|
4
|
+
:refunded_at,
|
5
|
+
:card_token,
|
6
|
+
:issuer,
|
7
|
+
:last_digits,
|
8
|
+
:owner,
|
9
|
+
:bank,
|
10
|
+
:iban,
|
11
|
+
:swift_bic,
|
12
|
+
:payment_id
|
13
|
+
|
14
|
+
alias_method :type, :kind
|
15
|
+
|
16
|
+
def initialize(attributes = {}, parent = nil)
|
17
|
+
if attributes
|
18
|
+
self.paid_at = DateTime.parse(attributes.delete('paid_at')) unless attributes['paid_at'].blank?
|
19
|
+
self.refunded_at = DateTime.parse(attributes.delete('refunded_at')) unless attributes['refunded_at'].blank?
|
20
|
+
end
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def paid?
|
26
|
+
self.paid_at.is_a?(DateTime)
|
27
|
+
end
|
28
|
+
|
29
|
+
def refunded?
|
30
|
+
self.refunded_at.is_a?(DateTime)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Cornerstore::Price < Cornerstore::Model::Base
|
2
|
+
include Comparable
|
3
|
+
|
4
|
+
attr_accessor :gross,
|
5
|
+
:net,
|
6
|
+
:tax,
|
7
|
+
:tax_rate,
|
8
|
+
:currency,
|
9
|
+
:amount
|
10
|
+
|
11
|
+
validates :gross, numericality: { greater_than_or_equal_to: 0 }, if: Proc.new { |p| not p.amount }
|
12
|
+
validates :amount, numericality: { greater_than_or_equal_to: 0 }, if: Proc.new { |p| not p.gross }
|
13
|
+
validates :net, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
14
|
+
validates :tax_rate, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
15
|
+
validates :currency, presence: true, inclusion: { in: %w( EUR USD ) }
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
if gross or tax_rate
|
19
|
+
{
|
20
|
+
gross: gross,
|
21
|
+
net: net,
|
22
|
+
tax_rate: tax_rate,
|
23
|
+
currency: currency
|
24
|
+
}
|
25
|
+
else
|
26
|
+
{
|
27
|
+
amount: amount,
|
28
|
+
currency: currency
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def <=> (other_object)
|
34
|
+
case other_object
|
35
|
+
when Integer, Float, Fixnum
|
36
|
+
self.amount <=> other_object
|
37
|
+
when Cornerstore::Price
|
38
|
+
self.amount <=> other_object.amount
|
39
|
+
else
|
40
|
+
raise ArgumentError, 'can only compare Integer, Float, Fixnum or Price objects with Price'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def +(other_object)
|
45
|
+
return self if not other_object or other_object == 0
|
46
|
+
|
47
|
+
if gross
|
48
|
+
Cornerstore::Price.new({
|
49
|
+
gross: gross + other_object.gross,
|
50
|
+
net: net + other_object.net,
|
51
|
+
currency: currency,
|
52
|
+
tax_rate: other_object.tax_rate == tax_rate ? tax_rate : nil
|
53
|
+
})
|
54
|
+
else
|
55
|
+
Cornerstore::Price.new({
|
56
|
+
amount: amount + other_object.amount,
|
57
|
+
currency: currency
|
58
|
+
})
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_f
|
64
|
+
@amount || @gross
|
65
|
+
end
|
66
|
+
|
67
|
+
def amount
|
68
|
+
@amount || @gross
|
69
|
+
end
|
70
|
+
|
71
|
+
def currency_symbol
|
72
|
+
{
|
73
|
+
'USD' => '$',
|
74
|
+
'EUR' => '€',
|
75
|
+
}[currency]
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class Cornerstore::Product < Cornerstore::Model::Base
|
2
|
+
attr_accessor :name,
|
3
|
+
:description,
|
4
|
+
:manufacturer,
|
5
|
+
:enabled,
|
6
|
+
:variants,
|
7
|
+
:images,
|
8
|
+
:properties
|
9
|
+
|
10
|
+
def initialize(attributes = {}, parent = nil)
|
11
|
+
self.images = Cornerstore::Image::Resource.new(self, attributes.delete('images') || [], 'images')
|
12
|
+
self.variants = Cornerstore::Variant::Resource.new(self, attributes.delete('variants') || [], 'variants')
|
13
|
+
self.properties = Cornerstore::Property::Resource.new(self, attributes.delete('properties') || [], 'properties')
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
{
|
19
|
+
name: name,
|
20
|
+
description: description,
|
21
|
+
manufacturer: manufacturer,
|
22
|
+
enabled: enabled
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def price
|
27
|
+
variants.collect { |v| v.price }.sort.first
|
28
|
+
end
|
29
|
+
|
30
|
+
def order_number
|
31
|
+
variants.many? ? :many : variants.first.order_number
|
32
|
+
end
|
33
|
+
|
34
|
+
def sold_out?
|
35
|
+
variants.all? { |v| v.sold_out? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def offer?
|
39
|
+
variants.any? { |v| v.offer? }
|
40
|
+
end
|
41
|
+
|
42
|
+
def new?
|
43
|
+
self.created_at >= 2.weeks.ago
|
44
|
+
end
|
45
|
+
|
46
|
+
def cover_image
|
47
|
+
@cover_image ||= self.images.to_a.find {|i| i.cover == true }
|
48
|
+
end
|
49
|
+
|
50
|
+
class Resource < Cornerstore::Resource::Base
|
51
|
+
include Cornerstore::Resource::Remote
|
52
|
+
include Cornerstore::Resource::Filter
|
53
|
+
|
54
|
+
def enabled
|
55
|
+
self.clone.set_filter(:enabled, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def by_collection(collection_id)
|
59
|
+
self.clone.tap{|r| r.parent = Cornerstore::Collection.find(collection_id)}
|
60
|
+
end
|
61
|
+
alias find_by_collection by_collection
|
62
|
+
|
63
|
+
def order(key)
|
64
|
+
first = %w(name popularity created_at price)
|
65
|
+
second = %w(desc asc)
|
66
|
+
pattern = /\A(#{first.join('|')})( (#{second.join('|')}))?\z/i
|
67
|
+
raise "order key must be one of: #{first.join(', ')} [#{second.join(', ')}]" unless key.match pattern
|
68
|
+
super
|
69
|
+
end
|
70
|
+
alias order_by order
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Cornerstore::Property < Cornerstore::Model::Base
|
2
|
+
include Cornerstore::Model::Writable
|
3
|
+
|
4
|
+
attr_accessor :key,
|
5
|
+
:value
|
6
|
+
|
7
|
+
def attributes
|
8
|
+
{
|
9
|
+
key: key,
|
10
|
+
value: value
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: Rewrite #url on base model to automatically detect parent/url depth.
|
15
|
+
def url
|
16
|
+
super(2)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(attributes = {}, parent = nil)
|
20
|
+
@key = attributes.delete(:key)
|
21
|
+
@value = attributes.delete(:value)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
class Resource < Cornerstore::Resource::Base
|
27
|
+
include Cornerstore::Resource::Remote
|
28
|
+
include Cornerstore::Resource::Writable
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Cornerstore::Search
|
2
|
+
extend ActiveModel::Naming
|
3
|
+
|
4
|
+
attr_accessor :keywords, :products, :scores
|
5
|
+
alias :results :products
|
6
|
+
|
7
|
+
def initialize(keywords = nil)
|
8
|
+
@keywords, @scores, @products = keywords, Array.new, Cornerstore::Product::Resource.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def results?
|
12
|
+
!@products.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_key
|
16
|
+
['']
|
17
|
+
end
|
18
|
+
|
19
|
+
def persisted?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
return false unless @keywords
|
25
|
+
|
26
|
+
RestClient.get("#{Cornerstore.root_url}/products/search?keywords=#{URI::encode(@keywords)}", Cornerstore.headers) do |response, request, result, &block|
|
27
|
+
if response.code == 200
|
28
|
+
data = ActiveSupport::JSON.decode(response)
|
29
|
+
@scores = data['scores']
|
30
|
+
@products = Cornerstore::Product::Resource.new(nil, data['products'])
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Cornerstore::Shipment < Cornerstore::Model::Base
|
2
|
+
attr_accessor :carrier,
|
3
|
+
:tracking_number,
|
4
|
+
:created_at,
|
5
|
+
:line_item_ids
|
6
|
+
|
7
|
+
alias_method :shipped_at, :created_at
|
8
|
+
|
9
|
+
def initialize(attributes = {}, parent=nil)
|
10
|
+
self.carrier = Cornerstore::Carrier.new(attributes.delete('carrier'))
|
11
|
+
self.line_item_ids = attributes.delete('shipped_items')
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def line_items
|
16
|
+
return [] unless self.parent
|
17
|
+
self.parent.line_items.select { |li| self.line_item_ids.include?(li.id) }
|
18
|
+
end
|
19
|
+
alias_method :shipped_items, :line_items
|
20
|
+
|
21
|
+
|
22
|
+
class Resource < Cornerstore::Resource::Base
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Cornerstore::Variant < Cornerstore::Model::Base
|
2
|
+
attr_accessor :order_number,
|
3
|
+
:price,
|
4
|
+
:qty_in_stock,
|
5
|
+
:qty_reserved,
|
6
|
+
:offer,
|
7
|
+
:oversell,
|
8
|
+
:weight,
|
9
|
+
:unit,
|
10
|
+
:properties,
|
11
|
+
:differentiating_properties
|
12
|
+
|
13
|
+
alias product parent
|
14
|
+
alias offer? offer
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
{
|
18
|
+
order_number: order_number,
|
19
|
+
price: price,
|
20
|
+
qty_in_stock: qty_in_stock,
|
21
|
+
qty_reserved: qty_reserved,
|
22
|
+
offer: offer,
|
23
|
+
oversell: oversell,
|
24
|
+
weight: weight,
|
25
|
+
unit: unit
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(attributes = {}, parent = nil)
|
30
|
+
self.price = Cornerstore::Price.new(attributes.delete('price'))
|
31
|
+
self.properties = Cornerstore::Property::Resource.new(self, attributes.delete('properties') || [])
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def qty_available
|
36
|
+
if self.qty_in_stock and not self.oversell
|
37
|
+
self.qty_in_stock - self.qty_reserved
|
38
|
+
else
|
39
|
+
Float::INFINITY
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def sold_out?
|
44
|
+
self.qty_available == 0
|
45
|
+
end
|
46
|
+
|
47
|
+
class Resource < Cornerstore::Resource::Base
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Cornerstore
|
2
|
+
module Model
|
3
|
+
class Base
|
4
|
+
include ActiveModel::Validations
|
5
|
+
include ActiveModel::Serializers::JSON
|
6
|
+
|
7
|
+
attr_accessor :_id
|
8
|
+
attr_accessor :_slugs
|
9
|
+
attr_accessor :parent
|
10
|
+
attr_accessor :created_at
|
11
|
+
attr_accessor :updated_at
|
12
|
+
|
13
|
+
alias id _id
|
14
|
+
|
15
|
+
def initialize(attributes = {}, parent = nil)
|
16
|
+
self.attributes = attributes
|
17
|
+
self.parent = parent
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_param
|
22
|
+
if _slugs and !_slugs.empty?
|
23
|
+
_slugs.first
|
24
|
+
else
|
25
|
+
_id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
other.id == self.id
|
31
|
+
end
|
32
|
+
|
33
|
+
alias eql? ==
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
{class: self.class.name, id: id}.merge!(attributes).to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def attributes
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
|
43
|
+
def attributes=(attributes)
|
44
|
+
attributes ||= {}
|
45
|
+
attributes.each_pair do |name, value|
|
46
|
+
send("#{name}=", value) if respond_to?("#{name}=")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def url(depth = 1)
|
51
|
+
root = (@parent && depth > 0) ? @parent.url(depth-1) : Cornerstore.root_url
|
52
|
+
"#{root}/#{self.class.name.split('::').last.underscore.pluralize}/#{id}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.method_missing(method, *args, &block)
|
56
|
+
if (self.const_defined?("Resource") and self.const_get("Resource").method_defined?(method))
|
57
|
+
self.const_get("Resource").new.send(method, *args, &block)
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_missing(method, *args, &block)
|
64
|
+
if Writable.method_defined?(method)
|
65
|
+
raise "Sorry, this part of the Cornerstore-API is currently read-only"
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Writable
|
73
|
+
def new?
|
74
|
+
id.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
alias new_record? new?
|
78
|
+
|
79
|
+
def to_key
|
80
|
+
new? ? [id] : nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def save
|
84
|
+
return false unless valid?
|
85
|
+
wrapped_attributes = {self.class.name.split('::').last.underscore => self.attributes}
|
86
|
+
if new?
|
87
|
+
response = RestClient.post(url, wrapped_attributes, Cornerstore.headers){|response| response}
|
88
|
+
self.attributes = ActiveSupport::JSON.decode(response)
|
89
|
+
else
|
90
|
+
response = RestClient.patch(url, wrapped_attributes, Cornerstore.headers){|response| response}
|
91
|
+
end
|
92
|
+
|
93
|
+
if response.success?
|
94
|
+
return true
|
95
|
+
else
|
96
|
+
if data = ActiveSupport::JSON.decode(response) and data.has_key?('errors')
|
97
|
+
data['errors'].each_pair do |key, messages|
|
98
|
+
messages.map { |message| self.errors.add(key, message) }
|
99
|
+
end
|
100
|
+
|
101
|
+
return false
|
102
|
+
else
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def destroy
|
109
|
+
RestClient.delete(url, Cornerstore.headers).success?
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.create(attributes = {}, &block)
|
113
|
+
self.new(attributes, &block).tap{|obj| obj.save}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|