glysellin 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (173) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +100 -0
  4. data/Rakefile +29 -0
  5. data/app/assets/javascripts/glysellin/address.coffee +38 -0
  6. data/app/assets/javascripts/glysellin/async-cart.coffee +139 -0
  7. data/app/assets/javascripts/glysellin/base.js +6 -0
  8. data/app/assets/javascripts/glysellin/cart.coffee +54 -0
  9. data/app/assets/javascripts/glysellin/main.coffee +0 -0
  10. data/app/assets/javascripts/glysellin/product.coffee +0 -0
  11. data/app/assets/stylesheets/glysellin/application.css +13 -0
  12. data/app/assets/stylesheets/glysellin/orders.css +4 -0
  13. data/app/assets/stylesheets/glysellin/products.css +4 -0
  14. data/app/controllers/glysellin/cart/addresses_controller.rb +17 -0
  15. data/app/controllers/glysellin/cart/discount_code_controller.rb +10 -0
  16. data/app/controllers/glysellin/cart/payment_method_controller.rb +17 -0
  17. data/app/controllers/glysellin/cart/products_controller.rb +37 -0
  18. data/app/controllers/glysellin/cart/shipping_method_controller.rb +18 -0
  19. data/app/controllers/glysellin/cart/state_controller.rb +15 -0
  20. data/app/controllers/glysellin/cart_controller.rb +53 -0
  21. data/app/controllers/glysellin/orders_controller.rb +53 -0
  22. data/app/controllers/glysellin/products_controller.rb +12 -0
  23. data/app/helpers/glysellin/application_helper.rb +4 -0
  24. data/app/helpers/glysellin/cart_helper.rb +21 -0
  25. data/app/helpers/glysellin/orders_helper.rb +35 -0
  26. data/app/helpers/glysellin/products_helper.rb +4 -0
  27. data/app/mailers/glysellin/order_admin_mailer.rb +17 -0
  28. data/app/mailers/glysellin/order_customer_mailer.rb +27 -0
  29. data/app/models/glysellin/address.rb +35 -0
  30. data/app/models/glysellin/brand.rb +14 -0
  31. data/app/models/glysellin/discount_code.rb +36 -0
  32. data/app/models/glysellin/discount_type.rb +9 -0
  33. data/app/models/glysellin/order.rb +243 -0
  34. data/app/models/glysellin/order_adjustment.rb +19 -0
  35. data/app/models/glysellin/order_item.rb +38 -0
  36. data/app/models/glysellin/orderer.rb +28 -0
  37. data/app/models/glysellin/payment.rb +46 -0
  38. data/app/models/glysellin/payment_method.rb +36 -0
  39. data/app/models/glysellin/product.rb +136 -0
  40. data/app/models/glysellin/product_image.rb +11 -0
  41. data/app/models/glysellin/product_property.rb +22 -0
  42. data/app/models/glysellin/product_property_type.rb +11 -0
  43. data/app/models/glysellin/product_type.rb +11 -0
  44. data/app/models/glysellin/shipping_method.rb +22 -0
  45. data/app/models/glysellin/taxonomy.rb +35 -0
  46. data/app/models/glysellin/variant.rb +100 -0
  47. data/app/views/glysellin/cart/_added_to_cart_warning.html.erb +21 -0
  48. data/app/views/glysellin/cart/_cart.html.erb +13 -0
  49. data/app/views/glysellin/cart/_navigation.html.erb +11 -0
  50. data/app/views/glysellin/cart/_products.html.erb +125 -0
  51. data/app/views/glysellin/cart/_recap.html.erb +16 -0
  52. data/app/views/glysellin/cart/recap/_addresses.html.erb +13 -0
  53. data/app/views/glysellin/cart/recap/_products.html.erb +63 -0
  54. data/app/views/glysellin/cart/show.html.erb +13 -0
  55. data/app/views/glysellin/cart/states/_addresses.html.erb +25 -0
  56. data/app/views/glysellin/cart/states/_choose_payment_method.html.erb +14 -0
  57. data/app/views/glysellin/cart/states/_choose_shipping_method.html.erb +14 -0
  58. data/app/views/glysellin/cart/states/_filled.html.erb +3 -0
  59. data/app/views/glysellin/cart/states/_init.html.erb +5 -0
  60. data/app/views/glysellin/cart/states/_ready.html.erb +1 -0
  61. data/app/views/glysellin/order_admin_mailer/_order_header.text.erb +5 -0
  62. data/app/views/glysellin/order_admin_mailer/send_check_order_created_email.text.erb +8 -0
  63. data/app/views/glysellin/order_admin_mailer/send_order_paid_email.text.erb +8 -0
  64. data/app/views/glysellin/order_customer_mailer/_order_footer.text.erb +3 -0
  65. data/app/views/glysellin/order_customer_mailer/_order_header.text.erb +5 -0
  66. data/app/views/glysellin/order_customer_mailer/send_order_created_email.text.erb +7 -0
  67. data/app/views/glysellin/order_customer_mailer/send_order_paid_email.text.erb +7 -0
  68. data/app/views/glysellin/order_customer_mailer/send_order_shipped_email.text.erb +8 -0
  69. data/app/views/glysellin/orders/_addresses_fields.html.erb +29 -0
  70. data/app/views/glysellin/orders/_order.html.erb +18 -0
  71. data/app/views/glysellin/orders/_payment_canceled.html.erb +13 -0
  72. data/app/views/glysellin/orders/_payment_successful.html.erb +6 -0
  73. data/app/views/glysellin/orders/payment_response.html.erb +5 -0
  74. data/app/views/glysellin/payment_methods/_paypal-integral.html.erb +22 -0
  75. data/app/views/glysellin/products/_add_to_cart.html.erb +16 -0
  76. data/app/views/glysellin/products/_item.html.erb +21 -0
  77. data/app/views/glysellin/products/filter.html.erb +0 -0
  78. data/app/views/glysellin/products/index.html.erb +3 -0
  79. data/app/views/glysellin/products/show.html.erb +3 -0
  80. data/app/views/glysellin/shared/_address.html.erb +11 -0
  81. data/app/views/glysellin/shared/_address.text.erb +7 -0
  82. data/app/views/glysellin/shared/_order.text.erb +56 -0
  83. data/app/views/glysellin/shared/mailer/_footer.text.erb +2 -0
  84. data/app/views/layouts/glysellin/application.html.erb +14 -0
  85. data/config/locales/glysellin.en.yml +113 -0
  86. data/config/locales/glysellin.fr.yml +119 -0
  87. data/db/migrate/20120206115004_create_glysellin_orders.rb +16 -0
  88. data/db/migrate/20120206115132_create_glysellin_customers.rb +8 -0
  89. data/db/migrate/20120206115657_create_glysellin_addresses.rb +22 -0
  90. data/db/migrate/20120206124505_create_glysellin_products.rb +20 -0
  91. data/db/migrate/20120206124545_create_glysellin_order_items.rb +15 -0
  92. data/db/migrate/20120206124630_create_glysellin_product_images.rb +10 -0
  93. data/db/migrate/20120206125210_add_attachment_image_to_product_image.rb +15 -0
  94. data/db/migrate/20120206130920_create_glysellin_payments.rb +13 -0
  95. data/db/migrate/20120206131042_create_glysellin_payment_methods.rb +10 -0
  96. data/db/migrate/20120207175050_create_glysellin_taxonomies.rb +11 -0
  97. data/db/migrate/20120207175158_create_products_taxonomies_table.rb +12 -0
  98. data/db/migrate/20120515143231_create_brands.rb +9 -0
  99. data/db/migrate/20120515143233_add_attachment_image_to_brands.rb +15 -0
  100. data/db/migrate/20120620151705_create_product_properties.rb +12 -0
  101. data/db/migrate/20130204121211_create_glysellin_discount_types.rb +10 -0
  102. data/db/migrate/20130204121228_create_glysellin_discount_codes.rb +15 -0
  103. data/db/migrate/20130204125310_create_glysellin_order_adjustments.rb +13 -0
  104. data/db/migrate/20130204202102_create_glysellin_shipping_methods.rb +12 -0
  105. data/db/migrate/20130204202109_add_shipping_method_id_to_glysellin_orders.rb +5 -0
  106. data/db/migrate/20130205110036_add_weight_to_glysellin_products.rb +5 -0
  107. data/db/migrate/20130205120541_add_weight_to_glysellin_order_items.rb +5 -0
  108. data/db/migrate/20130208162813_create_glysellin_variants.rb +17 -0
  109. data/db/migrate/20130208165020_create_glysellin_product_property_types.rb +7 -0
  110. data/db/migrate/20130211105557_add_type_id_to_glysellin_product_properties.rb +5 -0
  111. data/db/migrate/20130211112302_add_variant_id_to_glysellin_product_properties.rb +13 -0
  112. data/db/migrate/20130211152651_add_product_id_to_glysellin_variants.rb +5 -0
  113. data/db/migrate/20130212131658_add_weight_to_glysellin_variants.rb +5 -0
  114. data/db/migrate/20130212132253_remove_variant_columns_from_glysellin_products.rb +15 -0
  115. data/db/migrate/20130212154943_remove_eot_price_from_glysellin_products.rb +9 -0
  116. data/db/migrate/20130214184204_create_glysellin_product_types.rb +9 -0
  117. data/db/migrate/20130214184352_create_product_type_behavior.rb +15 -0
  118. data/db/migrate/20130221102200_add_unmarked_price_to_glysellin_variants.rb +5 -0
  119. data/db/migrate/20130311135842_add_addressable_to_glysellin_addresses.rb +6 -0
  120. data/db/migrate/20130314114048_change_addressable_associations_in_glysellin_addresses.rb +15 -0
  121. data/db/migrate/20130325113903_remove_address_ids_from_glysellin_orders.rb +11 -0
  122. data/db/seeds/shipping_carrier/rates/colissimo.csv +16 -0
  123. data/db/seeds/shipping_carrier/rates/lettre-max.csv +9 -0
  124. data/lib/active_model/model.rb +101 -0
  125. data/lib/generators/glysellin/install/USAGE +8 -0
  126. data/lib/generators/glysellin/install/install_generator.rb +50 -0
  127. data/lib/generators/glysellin/install/templates/initializer.rb +119 -0
  128. data/lib/generators/glysellin/install/templates/order_observer.rb +12 -0
  129. data/lib/generators/glysellin/install/utils.rb +11 -0
  130. data/lib/glysellin.rb +124 -0
  131. data/lib/glysellin/acts_as_sellable.rb +14 -0
  132. data/lib/glysellin/cart.rb +21 -0
  133. data/lib/glysellin/cart/address.rb +16 -0
  134. data/lib/glysellin/cart/adjustment.rb +17 -0
  135. data/lib/glysellin/cart/adjustment/base.rb +41 -0
  136. data/lib/glysellin/cart/adjustment/discount_code.rb +43 -0
  137. data/lib/glysellin/cart/adjustment/shipping_method.rb +31 -0
  138. data/lib/glysellin/cart/basket.rb +482 -0
  139. data/lib/glysellin/cart/customer.rb +36 -0
  140. data/lib/glysellin/cart/model_wrapper.rb +90 -0
  141. data/lib/glysellin/cart/nested_resource.rb +39 -0
  142. data/lib/glysellin/cart/payment_method.rb +8 -0
  143. data/lib/glysellin/cart/product.rb +16 -0
  144. data/lib/glysellin/cart/select.rb +31 -0
  145. data/lib/glysellin/cart/serializable.rb +30 -0
  146. data/lib/glysellin/cart/shipping_method.rb +8 -0
  147. data/lib/glysellin/discount_type_calculator.rb +7 -0
  148. data/lib/glysellin/discount_type_calculator/base.rb +30 -0
  149. data/lib/glysellin/discount_type_calculator/order_percentage.rb +16 -0
  150. data/lib/glysellin/engine.rb +14 -0
  151. data/lib/glysellin/engine/routes.rb +61 -0
  152. data/lib/glysellin/gateway.rb +16 -0
  153. data/lib/glysellin/gateway/atos.rb +111 -0
  154. data/lib/glysellin/gateway/base.rb +43 -0
  155. data/lib/glysellin/gateway/check.rb +34 -0
  156. data/lib/glysellin/gateway/paypal_integral.rb +71 -0
  157. data/lib/glysellin/helpers.rb +15 -0
  158. data/lib/glysellin/helpers/controller.rb +22 -0
  159. data/lib/glysellin/helpers/countries.rb +261 -0
  160. data/lib/glysellin/helpers/views.rb +28 -0
  161. data/lib/glysellin/product_methods.rb +66 -0
  162. data/lib/glysellin/products_list.rb +72 -0
  163. data/lib/glysellin/property_finder.rb +13 -0
  164. data/lib/glysellin/shipping_carrier.rb +14 -0
  165. data/lib/glysellin/shipping_carrier/base.rb +55 -0
  166. data/lib/glysellin/shipping_carrier/colissimo.rb +19 -0
  167. data/lib/glysellin/shipping_carrier/flat_rate.rb +19 -0
  168. data/lib/glysellin/shipping_carrier/free_shipping.rb +14 -0
  169. data/lib/glysellin/shipping_carrier/helpers/country_weight_table.rb +65 -0
  170. data/lib/glysellin/shipping_carrier/lettre_max.rb +19 -0
  171. data/lib/glysellin/version.rb +3 -0
  172. data/lib/tasks/glysellin_tasks.rake +37 -0
  173. 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
@@ -0,0 +1,11 @@
1
+ module Glysellin
2
+ module Generators
3
+ module Utils
4
+ module InstanceMethods
5
+ def do_say str, color = :green
6
+ say "[ Glysellin ] #{str}", color
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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