glysellin 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +20 -0
- data/README.md +100 -0
- data/Rakefile +29 -0
- data/app/assets/javascripts/glysellin/address.coffee +38 -0
- data/app/assets/javascripts/glysellin/async-cart.coffee +139 -0
- data/app/assets/javascripts/glysellin/base.js +6 -0
- data/app/assets/javascripts/glysellin/cart.coffee +54 -0
- data/app/assets/javascripts/glysellin/main.coffee +0 -0
- data/app/assets/javascripts/glysellin/product.coffee +0 -0
- data/app/assets/stylesheets/glysellin/application.css +13 -0
- data/app/assets/stylesheets/glysellin/orders.css +4 -0
- data/app/assets/stylesheets/glysellin/products.css +4 -0
- data/app/controllers/glysellin/cart/addresses_controller.rb +17 -0
- data/app/controllers/glysellin/cart/discount_code_controller.rb +10 -0
- data/app/controllers/glysellin/cart/payment_method_controller.rb +17 -0
- data/app/controllers/glysellin/cart/products_controller.rb +37 -0
- data/app/controllers/glysellin/cart/shipping_method_controller.rb +18 -0
- data/app/controllers/glysellin/cart/state_controller.rb +15 -0
- data/app/controllers/glysellin/cart_controller.rb +53 -0
- data/app/controllers/glysellin/orders_controller.rb +53 -0
- data/app/controllers/glysellin/products_controller.rb +12 -0
- data/app/helpers/glysellin/application_helper.rb +4 -0
- data/app/helpers/glysellin/cart_helper.rb +21 -0
- data/app/helpers/glysellin/orders_helper.rb +35 -0
- data/app/helpers/glysellin/products_helper.rb +4 -0
- data/app/mailers/glysellin/order_admin_mailer.rb +17 -0
- data/app/mailers/glysellin/order_customer_mailer.rb +27 -0
- data/app/models/glysellin/address.rb +35 -0
- data/app/models/glysellin/brand.rb +14 -0
- data/app/models/glysellin/discount_code.rb +36 -0
- data/app/models/glysellin/discount_type.rb +9 -0
- data/app/models/glysellin/order.rb +243 -0
- data/app/models/glysellin/order_adjustment.rb +19 -0
- data/app/models/glysellin/order_item.rb +38 -0
- data/app/models/glysellin/orderer.rb +28 -0
- data/app/models/glysellin/payment.rb +46 -0
- data/app/models/glysellin/payment_method.rb +36 -0
- data/app/models/glysellin/product.rb +136 -0
- data/app/models/glysellin/product_image.rb +11 -0
- data/app/models/glysellin/product_property.rb +22 -0
- data/app/models/glysellin/product_property_type.rb +11 -0
- data/app/models/glysellin/product_type.rb +11 -0
- data/app/models/glysellin/shipping_method.rb +22 -0
- data/app/models/glysellin/taxonomy.rb +35 -0
- data/app/models/glysellin/variant.rb +100 -0
- data/app/views/glysellin/cart/_added_to_cart_warning.html.erb +21 -0
- data/app/views/glysellin/cart/_cart.html.erb +13 -0
- data/app/views/glysellin/cart/_navigation.html.erb +11 -0
- data/app/views/glysellin/cart/_products.html.erb +125 -0
- data/app/views/glysellin/cart/_recap.html.erb +16 -0
- data/app/views/glysellin/cart/recap/_addresses.html.erb +13 -0
- data/app/views/glysellin/cart/recap/_products.html.erb +63 -0
- data/app/views/glysellin/cart/show.html.erb +13 -0
- data/app/views/glysellin/cart/states/_addresses.html.erb +25 -0
- data/app/views/glysellin/cart/states/_choose_payment_method.html.erb +14 -0
- data/app/views/glysellin/cart/states/_choose_shipping_method.html.erb +14 -0
- data/app/views/glysellin/cart/states/_filled.html.erb +3 -0
- data/app/views/glysellin/cart/states/_init.html.erb +5 -0
- data/app/views/glysellin/cart/states/_ready.html.erb +1 -0
- data/app/views/glysellin/order_admin_mailer/_order_header.text.erb +5 -0
- data/app/views/glysellin/order_admin_mailer/send_check_order_created_email.text.erb +8 -0
- data/app/views/glysellin/order_admin_mailer/send_order_paid_email.text.erb +8 -0
- data/app/views/glysellin/order_customer_mailer/_order_footer.text.erb +3 -0
- data/app/views/glysellin/order_customer_mailer/_order_header.text.erb +5 -0
- data/app/views/glysellin/order_customer_mailer/send_order_created_email.text.erb +7 -0
- data/app/views/glysellin/order_customer_mailer/send_order_paid_email.text.erb +7 -0
- data/app/views/glysellin/order_customer_mailer/send_order_shipped_email.text.erb +8 -0
- data/app/views/glysellin/orders/_addresses_fields.html.erb +29 -0
- data/app/views/glysellin/orders/_order.html.erb +18 -0
- data/app/views/glysellin/orders/_payment_canceled.html.erb +13 -0
- data/app/views/glysellin/orders/_payment_successful.html.erb +6 -0
- data/app/views/glysellin/orders/payment_response.html.erb +5 -0
- data/app/views/glysellin/payment_methods/_paypal-integral.html.erb +22 -0
- data/app/views/glysellin/products/_add_to_cart.html.erb +16 -0
- data/app/views/glysellin/products/_item.html.erb +21 -0
- data/app/views/glysellin/products/filter.html.erb +0 -0
- data/app/views/glysellin/products/index.html.erb +3 -0
- data/app/views/glysellin/products/show.html.erb +3 -0
- data/app/views/glysellin/shared/_address.html.erb +11 -0
- data/app/views/glysellin/shared/_address.text.erb +7 -0
- data/app/views/glysellin/shared/_order.text.erb +56 -0
- data/app/views/glysellin/shared/mailer/_footer.text.erb +2 -0
- data/app/views/layouts/glysellin/application.html.erb +14 -0
- data/config/locales/glysellin.en.yml +113 -0
- data/config/locales/glysellin.fr.yml +119 -0
- data/db/migrate/20120206115004_create_glysellin_orders.rb +16 -0
- data/db/migrate/20120206115132_create_glysellin_customers.rb +8 -0
- data/db/migrate/20120206115657_create_glysellin_addresses.rb +22 -0
- data/db/migrate/20120206124505_create_glysellin_products.rb +20 -0
- data/db/migrate/20120206124545_create_glysellin_order_items.rb +15 -0
- data/db/migrate/20120206124630_create_glysellin_product_images.rb +10 -0
- data/db/migrate/20120206125210_add_attachment_image_to_product_image.rb +15 -0
- data/db/migrate/20120206130920_create_glysellin_payments.rb +13 -0
- data/db/migrate/20120206131042_create_glysellin_payment_methods.rb +10 -0
- data/db/migrate/20120207175050_create_glysellin_taxonomies.rb +11 -0
- data/db/migrate/20120207175158_create_products_taxonomies_table.rb +12 -0
- data/db/migrate/20120515143231_create_brands.rb +9 -0
- data/db/migrate/20120515143233_add_attachment_image_to_brands.rb +15 -0
- data/db/migrate/20120620151705_create_product_properties.rb +12 -0
- data/db/migrate/20130204121211_create_glysellin_discount_types.rb +10 -0
- data/db/migrate/20130204121228_create_glysellin_discount_codes.rb +15 -0
- data/db/migrate/20130204125310_create_glysellin_order_adjustments.rb +13 -0
- data/db/migrate/20130204202102_create_glysellin_shipping_methods.rb +12 -0
- data/db/migrate/20130204202109_add_shipping_method_id_to_glysellin_orders.rb +5 -0
- data/db/migrate/20130205110036_add_weight_to_glysellin_products.rb +5 -0
- data/db/migrate/20130205120541_add_weight_to_glysellin_order_items.rb +5 -0
- data/db/migrate/20130208162813_create_glysellin_variants.rb +17 -0
- data/db/migrate/20130208165020_create_glysellin_product_property_types.rb +7 -0
- data/db/migrate/20130211105557_add_type_id_to_glysellin_product_properties.rb +5 -0
- data/db/migrate/20130211112302_add_variant_id_to_glysellin_product_properties.rb +13 -0
- data/db/migrate/20130211152651_add_product_id_to_glysellin_variants.rb +5 -0
- data/db/migrate/20130212131658_add_weight_to_glysellin_variants.rb +5 -0
- data/db/migrate/20130212132253_remove_variant_columns_from_glysellin_products.rb +15 -0
- data/db/migrate/20130212154943_remove_eot_price_from_glysellin_products.rb +9 -0
- data/db/migrate/20130214184204_create_glysellin_product_types.rb +9 -0
- data/db/migrate/20130214184352_create_product_type_behavior.rb +15 -0
- data/db/migrate/20130221102200_add_unmarked_price_to_glysellin_variants.rb +5 -0
- data/db/migrate/20130311135842_add_addressable_to_glysellin_addresses.rb +6 -0
- data/db/migrate/20130314114048_change_addressable_associations_in_glysellin_addresses.rb +15 -0
- data/db/migrate/20130325113903_remove_address_ids_from_glysellin_orders.rb +11 -0
- data/db/seeds/shipping_carrier/rates/colissimo.csv +16 -0
- data/db/seeds/shipping_carrier/rates/lettre-max.csv +9 -0
- data/lib/active_model/model.rb +101 -0
- data/lib/generators/glysellin/install/USAGE +8 -0
- data/lib/generators/glysellin/install/install_generator.rb +50 -0
- data/lib/generators/glysellin/install/templates/initializer.rb +119 -0
- data/lib/generators/glysellin/install/templates/order_observer.rb +12 -0
- data/lib/generators/glysellin/install/utils.rb +11 -0
- data/lib/glysellin.rb +124 -0
- data/lib/glysellin/acts_as_sellable.rb +14 -0
- data/lib/glysellin/cart.rb +21 -0
- data/lib/glysellin/cart/address.rb +16 -0
- data/lib/glysellin/cart/adjustment.rb +17 -0
- data/lib/glysellin/cart/adjustment/base.rb +41 -0
- data/lib/glysellin/cart/adjustment/discount_code.rb +43 -0
- data/lib/glysellin/cart/adjustment/shipping_method.rb +31 -0
- data/lib/glysellin/cart/basket.rb +482 -0
- data/lib/glysellin/cart/customer.rb +36 -0
- data/lib/glysellin/cart/model_wrapper.rb +90 -0
- data/lib/glysellin/cart/nested_resource.rb +39 -0
- data/lib/glysellin/cart/payment_method.rb +8 -0
- data/lib/glysellin/cart/product.rb +16 -0
- data/lib/glysellin/cart/select.rb +31 -0
- data/lib/glysellin/cart/serializable.rb +30 -0
- data/lib/glysellin/cart/shipping_method.rb +8 -0
- data/lib/glysellin/discount_type_calculator.rb +7 -0
- data/lib/glysellin/discount_type_calculator/base.rb +30 -0
- data/lib/glysellin/discount_type_calculator/order_percentage.rb +16 -0
- data/lib/glysellin/engine.rb +14 -0
- data/lib/glysellin/engine/routes.rb +61 -0
- data/lib/glysellin/gateway.rb +16 -0
- data/lib/glysellin/gateway/atos.rb +111 -0
- data/lib/glysellin/gateway/base.rb +43 -0
- data/lib/glysellin/gateway/check.rb +34 -0
- data/lib/glysellin/gateway/paypal_integral.rb +71 -0
- data/lib/glysellin/helpers.rb +15 -0
- data/lib/glysellin/helpers/controller.rb +22 -0
- data/lib/glysellin/helpers/countries.rb +261 -0
- data/lib/glysellin/helpers/views.rb +28 -0
- data/lib/glysellin/product_methods.rb +66 -0
- data/lib/glysellin/products_list.rb +72 -0
- data/lib/glysellin/property_finder.rb +13 -0
- data/lib/glysellin/shipping_carrier.rb +14 -0
- data/lib/glysellin/shipping_carrier/base.rb +55 -0
- data/lib/glysellin/shipping_carrier/colissimo.rb +19 -0
- data/lib/glysellin/shipping_carrier/flat_rate.rb +19 -0
- data/lib/glysellin/shipping_carrier/free_shipping.rb +14 -0
- data/lib/glysellin/shipping_carrier/helpers/country_weight_table.rb +65 -0
- data/lib/glysellin/shipping_carrier/lettre_max.rb +19 -0
- data/lib/glysellin/version.rb +3 -0
- data/lib/tasks/glysellin_tasks.rake +37 -0
- metadata +341 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
class OrderObserver < ActiveRecord::Observer
|
2
|
+
observe Glysellin::Order
|
3
|
+
|
4
|
+
def after_paid order, transition
|
5
|
+
# This will be run once the order has been paid
|
6
|
+
end
|
7
|
+
|
8
|
+
def after_shipped order, transition
|
9
|
+
# This will be run once the order has been shipped
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
data/lib/glysellin.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require "active_model/model"
|
2
|
+
require "paperclip"
|
3
|
+
|
4
|
+
require "glysellin/engine"
|
5
|
+
require "glysellin/helpers"
|
6
|
+
require "glysellin/gateway"
|
7
|
+
require "glysellin/discount_type_calculator"
|
8
|
+
require "glysellin/product_methods"
|
9
|
+
require "glysellin/property_finder"
|
10
|
+
require "glysellin/products_list"
|
11
|
+
require "glysellin/shipping_carrier"
|
12
|
+
require "glysellin/cart"
|
13
|
+
|
14
|
+
module Glysellin
|
15
|
+
################################################################
|
16
|
+
#
|
17
|
+
# Config vars, to be overriden from generated initializer file
|
18
|
+
#
|
19
|
+
################################################################
|
20
|
+
|
21
|
+
# Status const to be used to define order step to payment
|
22
|
+
ORDER_STEP_PAYMENT = 'payment'
|
23
|
+
|
24
|
+
################################################################
|
25
|
+
#
|
26
|
+
# Config vars, to be overriden from generated initializer file
|
27
|
+
#
|
28
|
+
################################################################
|
29
|
+
|
30
|
+
# Public: Defines which user class will be used to bind Customer model to an
|
31
|
+
# authenticable user
|
32
|
+
mattr_accessor :user_class_name
|
33
|
+
@@user_class_name = 'User'
|
34
|
+
|
35
|
+
# Public: Defines if SKU must be automatically set on product save
|
36
|
+
mattr_accessor :autoset_sku
|
37
|
+
@@autoset_sku = true
|
38
|
+
|
39
|
+
# Public: Defines which fields must be present when validating an
|
40
|
+
# Address model
|
41
|
+
mattr_accessor :address_presence_validation_keys
|
42
|
+
@@address_presence_validation_keys = [:first_name, :last_name, :address, :zip, :city, :country]
|
43
|
+
|
44
|
+
# Public: Has to be filled if there are additional address fields to
|
45
|
+
# store in database
|
46
|
+
mattr_accessor :additional_address_fields
|
47
|
+
@@additional_address_fields = []
|
48
|
+
|
49
|
+
# Public: Giving it a lambda will override Order reference
|
50
|
+
# string generation method
|
51
|
+
mattr_accessor :order_reference_generator
|
52
|
+
@@order_reference_generator = nil
|
53
|
+
|
54
|
+
# Public: Defines if we will show the subtotal field in Order recaps if
|
55
|
+
# no adjustements are applied to the Order price
|
56
|
+
mattr_accessor :show_subtotal_if_identical
|
57
|
+
@@show_subtotal_if_identical = false
|
58
|
+
|
59
|
+
# Public: Define if we should show shipping price if it is equal to zero
|
60
|
+
mattr_accessor :show_shipping_if_zero
|
61
|
+
@@show_shipping_if_zero = false
|
62
|
+
|
63
|
+
# Public: The sender e-mail address used in order e-mails
|
64
|
+
mattr_accessor :contact_email
|
65
|
+
@@contact_email = 'orders@example.com'
|
66
|
+
|
67
|
+
# Public: The destination e-mail for order validations and other
|
68
|
+
# admin related e-mails
|
69
|
+
mattr_accessor :admin_email
|
70
|
+
@@admin_email = 'admin@example.com'
|
71
|
+
|
72
|
+
# Public: Public shop name used when referencing it inside
|
73
|
+
# the app and e-mails
|
74
|
+
mattr_accessor :shop_name
|
75
|
+
@@shop_name = 'Example Shop Name'
|
76
|
+
|
77
|
+
mattr_accessor :default_vat_rate
|
78
|
+
@@default_vat_rate = 19.6
|
79
|
+
|
80
|
+
mattr_accessor :default_product_weight
|
81
|
+
@@default_product_weight = 0
|
82
|
+
|
83
|
+
mattr_accessor :send_email_on_check_order_placed
|
84
|
+
@@send_email_on_check_order_placed = true
|
85
|
+
|
86
|
+
mattr_accessor :async_cart
|
87
|
+
@@async_cart = true
|
88
|
+
|
89
|
+
mattr_accessor :step_routes
|
90
|
+
@@step_routes = {
|
91
|
+
payment_method_chosen: ORDER_STEP_PAYMENT
|
92
|
+
}
|
93
|
+
|
94
|
+
# Product images paperclip styles
|
95
|
+
mattr_accessor :product_images_styles
|
96
|
+
@@product_images_styles = {
|
97
|
+
:thumb => '100x100#',
|
98
|
+
:content => '300x300'
|
99
|
+
}
|
100
|
+
|
101
|
+
mattr_accessor :allow_anonymous_orders
|
102
|
+
@@allow_anonymous_orders = true
|
103
|
+
|
104
|
+
def self.mailer_subjects=(hash = nil)
|
105
|
+
@@mailer_subjects = -> { I18n.t('glysellin.mailer').merge(hash) }
|
106
|
+
end
|
107
|
+
|
108
|
+
mattr_accessor :mailer_subjects
|
109
|
+
@@mailer_subjects = -> { I18n.t('glysellin.mailer') }
|
110
|
+
|
111
|
+
# Public: Permits using config block in order to set
|
112
|
+
# Glysellin module attributes
|
113
|
+
#
|
114
|
+
# Examples
|
115
|
+
#
|
116
|
+
# Glysellin.config do |c|
|
117
|
+
# c.contact_email = 'orders@myshop.com'
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# Returns nothing
|
121
|
+
def self.config
|
122
|
+
yield self
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module ActsAsSellable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def acts_as_sellable
|
7
|
+
has_one :product, :class_name => "::Glysellin::Product", :inverse_of => :item
|
8
|
+
accepts_nested_attributes_for :product, :allow_destroy => true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
::ActiveRecord::Base.send :include, Glysellin::ActsAsSellable
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Glysellin
|
4
|
+
module Cart
|
5
|
+
def self.new cookie = nil, options = {}
|
6
|
+
Basket.new cookie, options
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'glysellin/cart/model_wrapper'
|
12
|
+
require 'glysellin/cart/nested_resource'
|
13
|
+
require 'glysellin/cart/select'
|
14
|
+
require 'glysellin/cart/serializable'
|
15
|
+
require 'glysellin/cart/product'
|
16
|
+
require 'glysellin/cart/customer'
|
17
|
+
require 'glysellin/cart/address'
|
18
|
+
require 'glysellin/cart/payment_method'
|
19
|
+
require 'glysellin/cart/shipping_method'
|
20
|
+
require 'glysellin/cart/adjustment'
|
21
|
+
require 'glysellin/cart/basket'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module Cart
|
3
|
+
class Address
|
4
|
+
include ModelWrapper
|
5
|
+
wraps :embed_address, class_name: "Glysellin::Address"
|
6
|
+
|
7
|
+
def as_json
|
8
|
+
attributes.reject do |attr, _|
|
9
|
+
key = attr.to_s
|
10
|
+
%w(id created_at updated_at activated).include?(key) ||
|
11
|
+
key.match(/addressable/)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module Cart
|
3
|
+
module Adjustment
|
4
|
+
mattr_accessor :types
|
5
|
+
@@types = {}
|
6
|
+
|
7
|
+
def self.build cart, attributes
|
8
|
+
type = attributes[:type]
|
9
|
+
Glysellin::Cart::Adjustment.types[type].new(cart, attributes)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'glysellin/cart/adjustment/base'
|
16
|
+
require 'glysellin/cart/adjustment/discount_code'
|
17
|
+
require 'glysellin/cart/adjustment/shipping_method'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module Cart
|
3
|
+
module Adjustment
|
4
|
+
class Base
|
5
|
+
include Serializable
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :type
|
9
|
+
|
10
|
+
def register type, klass
|
11
|
+
@type = type
|
12
|
+
Glysellin::Cart::Adjustment.types[type] = klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def inherited subclass
|
16
|
+
inheritable_accessors = @attrs.reject do |key|
|
17
|
+
[:name, :value, :cart].include?(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
subclass.attributes *inheritable_accessors
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attributes :name, :value, :type, :cart
|
25
|
+
|
26
|
+
def initialize cart, attributes
|
27
|
+
# Keep a reference to the cart to calculate discount
|
28
|
+
self.cart = cart
|
29
|
+
|
30
|
+
# Ensure type attribute is set
|
31
|
+
attributes[:type] = self.class.type
|
32
|
+
|
33
|
+
# Initialize attributes
|
34
|
+
attributes.each do |key, value|
|
35
|
+
self.public_send("#{ key }=", value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module Cart
|
3
|
+
module Adjustment
|
4
|
+
class DiscountCode < Glysellin::Cart::Adjustment::Base
|
5
|
+
register "discount-code", self
|
6
|
+
|
7
|
+
attributes :discount_code
|
8
|
+
attr_accessor :valid
|
9
|
+
|
10
|
+
def initialize cart, attributes
|
11
|
+
super cart, attributes
|
12
|
+
process_code
|
13
|
+
end
|
14
|
+
|
15
|
+
def process_code
|
16
|
+
code = Glysellin::DiscountCode.from_code(discount_code)
|
17
|
+
|
18
|
+
if code
|
19
|
+
self.valid = true
|
20
|
+
adjustment = code.to_adjustment(cart)
|
21
|
+
self.name = adjustment[:name]
|
22
|
+
self.value = adjustment[:value]
|
23
|
+
else
|
24
|
+
self.valid = false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_adjustment
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def from_code str, cart
|
34
|
+
if str.presence
|
35
|
+
discount = self.new(cart, discount_code: str)
|
36
|
+
discount.valid ? discount : nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Glysellin
|
2
|
+
module Cart
|
3
|
+
module Adjustment
|
4
|
+
class ShippingMethod < Glysellin::Cart::Adjustment::Base
|
5
|
+
register "shipping-method", self
|
6
|
+
|
7
|
+
attributes :shipping_method_id
|
8
|
+
|
9
|
+
attr_accessor :valid
|
10
|
+
|
11
|
+
def initialize cart, attributes = {}
|
12
|
+
super cart, attributes
|
13
|
+
|
14
|
+
adjustment = cart.shipping_method.to_adjustment(cart)
|
15
|
+
|
16
|
+
if adjustment[:value]
|
17
|
+
self.valid = true
|
18
|
+
self.name = adjustment[:name]
|
19
|
+
self.value = adjustment[:value]
|
20
|
+
else
|
21
|
+
self.valid = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_adjustment
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,482 @@
|
|
1
|
+
require "state_machine"
|
2
|
+
|
3
|
+
module Glysellin
|
4
|
+
module Cart
|
5
|
+
class Basket
|
6
|
+
include ProductsList
|
7
|
+
include Cart::NestedResource
|
8
|
+
include Cart::Select
|
9
|
+
include ActiveModel::Model
|
10
|
+
include ActiveModel::Dirty
|
11
|
+
include ActiveModel::Observing
|
12
|
+
|
13
|
+
attr_accessor :total, :products, :adjustments, :state, :order_id
|
14
|
+
attr_reader :use_another_address_for_shipping
|
15
|
+
|
16
|
+
# Relations
|
17
|
+
#
|
18
|
+
nested_resource :customer
|
19
|
+
nested_resource :billing_address, class_name: "Address"
|
20
|
+
nested_resource :shipping_address, class_name: "Address"
|
21
|
+
select :payment_method, class_name: "Glysellin::PaymentMethod"
|
22
|
+
select :shipping_method, class_name: "Glysellin::ShippingMethod"
|
23
|
+
|
24
|
+
# Checkout state management
|
25
|
+
#
|
26
|
+
define_attribute_methods [:state]
|
27
|
+
|
28
|
+
state_machine initial: :init do
|
29
|
+
event :reset do
|
30
|
+
transition all => :init
|
31
|
+
end
|
32
|
+
|
33
|
+
event :products_added do
|
34
|
+
transition all => :filled
|
35
|
+
end
|
36
|
+
|
37
|
+
# When cart content is validated, including products list
|
38
|
+
# and coupon codes
|
39
|
+
#
|
40
|
+
event :validated do
|
41
|
+
transition all => :addresses
|
42
|
+
end
|
43
|
+
|
44
|
+
# When addresses are filled and validated
|
45
|
+
#
|
46
|
+
event :addresses_filled do
|
47
|
+
transition all => :choose_shipping_method
|
48
|
+
end
|
49
|
+
|
50
|
+
# When shipping method is chosen
|
51
|
+
#
|
52
|
+
event :shipping_method_chosen do
|
53
|
+
transition all => :choose_payment_method
|
54
|
+
end
|
55
|
+
|
56
|
+
# When payment method is chosen
|
57
|
+
#
|
58
|
+
event :payment_method_chosen do
|
59
|
+
transition all => :ready
|
60
|
+
end
|
61
|
+
|
62
|
+
# State validations
|
63
|
+
state :products_added, :addresses, :choose_shipping_method, :choose_payment_method, :ready do
|
64
|
+
validates_presence_of :products
|
65
|
+
end
|
66
|
+
|
67
|
+
state :addresses, :choose_shipping_method, :choose_payment_method, :ready do
|
68
|
+
validate do
|
69
|
+
validate_customer_informations
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
state :choose_shipping_method, :choose_payment_method, :ready do
|
74
|
+
validates_presence_of :shipping_method_id
|
75
|
+
validate do
|
76
|
+
validate_shippable
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
state :choose_payment_method, :ready do
|
81
|
+
validates_presence_of :payment_method_id
|
82
|
+
end
|
83
|
+
|
84
|
+
after_transition any => :ready, do: :generate_order
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize str = nil, options = {}
|
88
|
+
data = parse_session(str, options)
|
89
|
+
super(data)
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_session str, options
|
93
|
+
data = str.presence ?
|
94
|
+
ActiveSupport::JSON.decode(str).with_indifferent_access :
|
95
|
+
{ adjustments: [], products: [] }
|
96
|
+
|
97
|
+
data.reverse_merge(options)
|
98
|
+
end
|
99
|
+
|
100
|
+
#############################################
|
101
|
+
#
|
102
|
+
# Cart content management
|
103
|
+
#
|
104
|
+
#############################################
|
105
|
+
|
106
|
+
# Used to implement ProductsList interface #each method
|
107
|
+
def quantified_items
|
108
|
+
products.map { |product| [product.variant, product.quantity] }
|
109
|
+
end
|
110
|
+
|
111
|
+
def empty?
|
112
|
+
products.length == 0
|
113
|
+
end
|
114
|
+
|
115
|
+
def products_total
|
116
|
+
products.reduce(0) { |total, product| total + product.quantity }
|
117
|
+
end
|
118
|
+
|
119
|
+
def product product_id
|
120
|
+
products.find { |product| product.variant.id == product_id.to_i }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Mass products assignement, from session hash
|
124
|
+
def products=(items)
|
125
|
+
if items.is_a? Array
|
126
|
+
@products = items.map do |product|
|
127
|
+
Product.new(product)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Mass adjustments assignement, from session hash
|
133
|
+
def adjustments=(items)
|
134
|
+
if items.is_a? Array
|
135
|
+
@adjustments = items.map do |adjustment|
|
136
|
+
Adjustment.build(self, adjustment)
|
137
|
+
end.compact
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def adjustments
|
142
|
+
@adjustments ||= []
|
143
|
+
end
|
144
|
+
|
145
|
+
#############################################
|
146
|
+
#
|
147
|
+
# Quantities management
|
148
|
+
#
|
149
|
+
#############################################
|
150
|
+
|
151
|
+
# Product scoped quantity update
|
152
|
+
#
|
153
|
+
def set_quantity product_id, quantity, options = {}
|
154
|
+
options.reverse_merge!(override: false)
|
155
|
+
quantity = quantity.to_i
|
156
|
+
|
157
|
+
return unless quantity > 0
|
158
|
+
|
159
|
+
# If product was in cart
|
160
|
+
if (product = self.product(product_id))
|
161
|
+
if options[:override]
|
162
|
+
product.quantity = quantity
|
163
|
+
else
|
164
|
+
product.quantity += quantity
|
165
|
+
end
|
166
|
+
else
|
167
|
+
@products << Glysellin::Cart::Product.new(
|
168
|
+
id: product_id,
|
169
|
+
quantity: quantity
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Refresh discount code adjustment
|
174
|
+
self.discount_code = discount_code
|
175
|
+
end
|
176
|
+
|
177
|
+
# Remove product from cart, given its id
|
178
|
+
#
|
179
|
+
def remove product_id
|
180
|
+
products.reject! { |product| product.variant.id == product_id.to_i }
|
181
|
+
end
|
182
|
+
|
183
|
+
# General check to see if cart is valid
|
184
|
+
#
|
185
|
+
def update_quantities!
|
186
|
+
@products = products.reduce([]) do |products, product|
|
187
|
+
# If product is not published
|
188
|
+
if !product.variant.published
|
189
|
+
set_error(:item_not_for_sale, item: product.variant.name)
|
190
|
+
# If product is not in stock
|
191
|
+
elsif !product.variant.in_stock?
|
192
|
+
set_error(:item_out_of_stock, item: product.variant.name)
|
193
|
+
# If product's available stock is less than required quantity
|
194
|
+
elsif !product.variant.available_for(product.quantity)
|
195
|
+
set_error(
|
196
|
+
:not_enough_stock_for_item,
|
197
|
+
item: product.variant.name, stock: product.variant.in_stock
|
198
|
+
)
|
199
|
+
product.quantity = product.variant.in_stock
|
200
|
+
products << product if product.quantity > 0
|
201
|
+
# Else, keep product as is in cart
|
202
|
+
else product.variant.unlimited_stock || product.variant.in_stock >= product.quantity
|
203
|
+
products << product
|
204
|
+
end
|
205
|
+
|
206
|
+
products
|
207
|
+
end
|
208
|
+
|
209
|
+
# TODO: Doesn't work, think in something else ...
|
210
|
+
if discount_code == false
|
211
|
+
set_error(:invalid_discount_code)
|
212
|
+
end
|
213
|
+
|
214
|
+
reset! if @products.length == 0
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
#############################################
|
219
|
+
#
|
220
|
+
# Quantity management
|
221
|
+
#
|
222
|
+
#############################################
|
223
|
+
|
224
|
+
def add(params)
|
225
|
+
set_quantity(params[:product_id], params[:quantity])
|
226
|
+
end
|
227
|
+
|
228
|
+
def update(attributes = {})
|
229
|
+
filtered_attributes(attributes).each do |attr, value|
|
230
|
+
public_send(:"#{ attr }=", value)
|
231
|
+
end if attributes
|
232
|
+
end
|
233
|
+
|
234
|
+
# Filters forbidden attributes
|
235
|
+
#
|
236
|
+
def filtered_attributes attributes
|
237
|
+
forbidden = [:id, :created_at, :updated_at]
|
238
|
+
|
239
|
+
# Filter aux function to browse hash tree and reject key by key
|
240
|
+
# forbidden attributes
|
241
|
+
#
|
242
|
+
filter = lambda { |hash, field|
|
243
|
+
key, value = field
|
244
|
+
|
245
|
+
if value.is_a? Hash
|
246
|
+
hash[key] = value.reduce({}, &filter)
|
247
|
+
else
|
248
|
+
hash[key] = value unless forbidden.include?(key)
|
249
|
+
end
|
250
|
+
|
251
|
+
hash
|
252
|
+
}
|
253
|
+
|
254
|
+
attributes.with_indifferent_access.reduce({}, &filter)
|
255
|
+
end
|
256
|
+
|
257
|
+
#############################################
|
258
|
+
#
|
259
|
+
# Discount code handling
|
260
|
+
#
|
261
|
+
#############################################
|
262
|
+
|
263
|
+
def discount_code=(val)
|
264
|
+
# Destroy old discount code
|
265
|
+
adjustments.delete(discount)
|
266
|
+
|
267
|
+
discount = Glysellin::Cart::Adjustment::DiscountCode.from_code(val, self)
|
268
|
+
adjustments << discount if discount
|
269
|
+
|
270
|
+
discount
|
271
|
+
end
|
272
|
+
|
273
|
+
def discount_code
|
274
|
+
(adjustment = discount) ? adjustment.discount_code : nil
|
275
|
+
end
|
276
|
+
|
277
|
+
def discount
|
278
|
+
adjustments.find { |a| a.type == "discount-code" }
|
279
|
+
end
|
280
|
+
|
281
|
+
#############################################
|
282
|
+
#
|
283
|
+
# Address
|
284
|
+
#
|
285
|
+
#############################################
|
286
|
+
|
287
|
+
# Allows setting use_another_address_for_shipping attribute, ensuring
|
288
|
+
# it is stored as a Boolean value and not a number string
|
289
|
+
#
|
290
|
+
def use_another_address_for_shipping=(val)
|
291
|
+
value = val.is_a?(String) ? (val.to_i > 0) : val
|
292
|
+
@use_another_address_for_shipping = value
|
293
|
+
end
|
294
|
+
|
295
|
+
# Set the shipping method id on the cart by creating the corresponding
|
296
|
+
# adjustment at the same time to ensure cart price and recap takes
|
297
|
+
# shipping costs into account
|
298
|
+
#
|
299
|
+
def shipping_method_id=(val)
|
300
|
+
@shipping_method_id = val
|
301
|
+
|
302
|
+
if shipping_method_id
|
303
|
+
adjustments.reject! { |a| a.type == "shipping-method" }
|
304
|
+
adjustment = Glysellin::Cart::Adjustment::ShippingMethod.new(self,
|
305
|
+
shipping_method_id: shipping_method_id
|
306
|
+
)
|
307
|
+
|
308
|
+
adjustments << adjustment
|
309
|
+
end
|
310
|
+
|
311
|
+
@shipping_method_id
|
312
|
+
end
|
313
|
+
|
314
|
+
# Shortcut method to get shipping adjustments from adjustments list
|
315
|
+
#
|
316
|
+
def shipping
|
317
|
+
adjustments.find { |a| a.type == "shipping-method" }
|
318
|
+
end
|
319
|
+
|
320
|
+
# Validates customer informations are correctly filled
|
321
|
+
#
|
322
|
+
def validate_customer_informations
|
323
|
+
validate_nested_resource(:customer)
|
324
|
+
validate_nested_resource(:billing_address)
|
325
|
+
|
326
|
+
if use_another_address_for_shipping
|
327
|
+
validate_nested_resource(:shipping_address)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Validates the selected country is eligible for the current cart contents
|
332
|
+
# to be shipped to
|
333
|
+
#
|
334
|
+
def validate_shippable
|
335
|
+
if !shipping || !shipping.valid
|
336
|
+
code = use_another_address_for_shipping ?
|
337
|
+
shipping_address.country : billing_address.country
|
338
|
+
country = Glysellin::Helpers::Countries::COUNTRIES_LIST[code]
|
339
|
+
|
340
|
+
errors.add(
|
341
|
+
:shipping_method_id,
|
342
|
+
I18n.t(
|
343
|
+
"glysellin.errors.cart.shipping_method_unavailable_for_country",
|
344
|
+
method: shipping_method.name,
|
345
|
+
country: country
|
346
|
+
)
|
347
|
+
)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
#############################################
|
352
|
+
#
|
353
|
+
# Order management
|
354
|
+
#
|
355
|
+
#############################################
|
356
|
+
|
357
|
+
# Generates an order from the current cart state and stores its id in
|
358
|
+
# the cart to be fetched back later
|
359
|
+
#
|
360
|
+
def generate_order
|
361
|
+
clean_order!
|
362
|
+
|
363
|
+
attrs = attributes(:json).reject do |key, _|
|
364
|
+
[:adjustments, :state, :order_id, :shipping_method_id].include?(key)
|
365
|
+
end
|
366
|
+
|
367
|
+
%w(billing shipping).each do |addr|
|
368
|
+
attrs[:"#{ addr }_address_attributes"] =
|
369
|
+
attrs.delete(:"#{ addr }_address")
|
370
|
+
end
|
371
|
+
|
372
|
+
unless use_another_address_for_shipping
|
373
|
+
attrs[:shipping_address_attributes] =
|
374
|
+
attrs[:billing_address_attributes]
|
375
|
+
end
|
376
|
+
|
377
|
+
# Append shipping method id after addresses so the latters are present
|
378
|
+
# when shipping adjustment is processed on order
|
379
|
+
attrs[:shipping_method_id] = shipping_method_id
|
380
|
+
|
381
|
+
attrs[:discount_code] = discount_code
|
382
|
+
|
383
|
+
self.order = Glysellin::Order.create!(attrs)
|
384
|
+
end
|
385
|
+
|
386
|
+
# Retrieve order from database if it exists, or use cached version
|
387
|
+
#
|
388
|
+
def order
|
389
|
+
@order ||= Glysellin::Order.where(id: order_id).first
|
390
|
+
end
|
391
|
+
|
392
|
+
# Assign order and order_id, if nil is explicitly passed, ensure we
|
393
|
+
# set order id to nil too
|
394
|
+
#
|
395
|
+
def order=(order)
|
396
|
+
self.order_id = order && order.id
|
397
|
+
@order = order
|
398
|
+
end
|
399
|
+
|
400
|
+
# Cleans cart stored order if it exists
|
401
|
+
#
|
402
|
+
def clean_order!
|
403
|
+
if order
|
404
|
+
# Destroy current cart order if not paid already, cause
|
405
|
+
# we're creating a new one
|
406
|
+
order.destroy if order.state_name == :ready
|
407
|
+
# unset order
|
408
|
+
self.order = nil
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
#############################################
|
413
|
+
#
|
414
|
+
# Serialization
|
415
|
+
#
|
416
|
+
#############################################
|
417
|
+
|
418
|
+
def attribute_names
|
419
|
+
[
|
420
|
+
:order_id, :products, :customer, :billing_address, :shipping_address,
|
421
|
+
:use_another_address_for_shipping, :shipping_method_id,
|
422
|
+
:payment_method_id, :adjustments, :state
|
423
|
+
]
|
424
|
+
end
|
425
|
+
|
426
|
+
def attributes(type = :ruby)
|
427
|
+
attribute_names.reduce({}) do |hash, attr|
|
428
|
+
value = send(attr)
|
429
|
+
hash[attr] = (type == :json) ? value.as_json : value
|
430
|
+
hash
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def serialize
|
435
|
+
attributes(:json).to_json
|
436
|
+
end
|
437
|
+
|
438
|
+
def to_order
|
439
|
+
Glysellin::Order.new
|
440
|
+
end
|
441
|
+
|
442
|
+
#############################################
|
443
|
+
#
|
444
|
+
# Errors
|
445
|
+
#
|
446
|
+
#############################################
|
447
|
+
|
448
|
+
def set_error(key, options = {})
|
449
|
+
errors.add(key, I18n.t("glysellin.errors.cart.#{ key }", options))
|
450
|
+
end
|
451
|
+
|
452
|
+
def validate_nested_resource key
|
453
|
+
model = send(key)
|
454
|
+
if !model.valid? && model.errors.any?
|
455
|
+
model.errors.messages.each do |field, messages|
|
456
|
+
messages.each do |error|
|
457
|
+
errors.add(:"#{ key }.#{ field }", error)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
#############################################
|
464
|
+
#
|
465
|
+
# States
|
466
|
+
#
|
467
|
+
#############################################
|
468
|
+
|
469
|
+
def available_states
|
470
|
+
%w(filled addresses choose_shipping_method choose_payment_method ready)
|
471
|
+
end
|
472
|
+
|
473
|
+
def state_index
|
474
|
+
available_states.index(state) || -1
|
475
|
+
end
|
476
|
+
|
477
|
+
def has_shipping_address?
|
478
|
+
use_another_address_for_shipping
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|