cornerstore 0.6.2
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 +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
|