stall 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -0
  3. data/app/assets/javascripts/stall.coffee +2 -0
  4. data/app/assets/javascripts/stall/cart-form.coffee +23 -0
  5. data/app/controllers/stall/application_controller.rb +10 -2
  6. data/app/controllers/stall/carts_controller.rb +31 -10
  7. data/app/controllers/stall/checkout/steps_controller.rb +36 -4
  8. data/app/controllers/stall/checkouts_controller.rb +12 -3
  9. data/app/controllers/stall/line_items_controller.rb +3 -3
  10. data/app/controllers/stall/payments_controller.rb +5 -3
  11. data/app/helpers/stall/add_to_cart_helper.rb +1 -1
  12. data/app/helpers/stall/checkout_helper.rb +22 -2
  13. data/app/helpers/stall/payments_helper.rb +8 -0
  14. data/app/mailers/stall/admin_mailer.rb +11 -0
  15. data/app/mailers/stall/base_mailer.rb +17 -0
  16. data/app/mailers/stall/customer_mailer.rb +13 -0
  17. data/app/models/address.rb +3 -0
  18. data/app/models/address_ownership.rb +3 -0
  19. data/app/models/adjustment.rb +3 -0
  20. data/app/models/cart.rb +3 -0
  21. data/app/models/customer.rb +3 -0
  22. data/app/models/line_item.rb +3 -0
  23. data/app/models/payment.rb +3 -0
  24. data/app/models/payment_method.rb +3 -0
  25. data/app/models/product_list.rb +3 -0
  26. data/app/models/shipment.rb +3 -0
  27. data/app/models/shipping_method.rb +3 -0
  28. data/app/models/stall/models.rb +4 -0
  29. data/app/models/stall/models/address.rb +45 -0
  30. data/app/models/stall/models/address_ownership.rb +26 -0
  31. data/app/models/stall/models/adjustment.rb +49 -0
  32. data/app/models/stall/models/cart.rb +37 -0
  33. data/app/models/stall/models/customer.rb +18 -0
  34. data/app/models/stall/models/line_item.rb +57 -0
  35. data/app/models/stall/models/payment.rb +33 -0
  36. data/app/models/stall/models/payment_method.rb +18 -0
  37. data/app/models/stall/models/product_list.rb +147 -0
  38. data/app/models/stall/models/shipment.rb +29 -0
  39. data/app/models/stall/models/shipping_method.rb +16 -0
  40. data/app/services/stall/add_to_cart_service.rb +11 -1
  41. data/app/services/stall/cart_update_service.rb +26 -0
  42. data/app/services/stall/payment_notification_service.rb +23 -9
  43. data/app/services/stall/shipping_fee_calculator_service.rb +13 -2
  44. data/app/views/checkout/steps/_informations.html.haml +3 -3
  45. data/app/views/checkout/steps/_payment.html.haml +25 -4
  46. data/app/views/checkout/steps/_payment_method.html.haml +1 -3
  47. data/app/views/checkout/steps/_payment_return.html.haml +6 -0
  48. data/app/views/checkout/steps/_shipping_method.html.haml +1 -1
  49. data/app/views/stall/admin_mailer/order_paid_email.html.haml +4 -0
  50. data/app/views/stall/carts/show.html.haml +36 -8
  51. data/app/views/stall/customer_mailer/order_paid_email.html.haml +8 -0
  52. data/app/views/stall/line_items/_added.html.haml +1 -1
  53. data/app/views/stall/shared/mailers/_address.html.haml +18 -0
  54. data/app/views/stall/shared/mailers/_cart.html.haml +114 -0
  55. data/config/locales/stall.fr.yml +90 -8
  56. data/db/migrate/20160304134849_change_all_json_columns_to_jsonb.rb +18 -0
  57. data/db/migrate/20160307142924_add_state_to_stall_addresses.rb +5 -0
  58. data/db/migrate/20160308142713_create_adjustments.rb +16 -0
  59. data/db/migrate/20160309165136_add_identifier_to_stall_product_lists.rb +5 -0
  60. data/db/migrate/20160316114649_add_data_to_stall_shipments.rb +5 -0
  61. data/db/migrate/20160317141632_add_state_to_stall_shipments.rb +5 -0
  62. data/db/migrate/20160629102943_add_active_to_stall_shipping_methods.rb +11 -0
  63. data/db/migrate/20160629104617_add_active_to_stall_payment_methods.rb +11 -0
  64. data/db/migrate/20160705110151_add_locale_to_stall_customers.rb +5 -0
  65. data/lib/generators/stall/checkout/step/templates/step.html.haml.erb +1 -1
  66. data/lib/generators/stall/checkout/step/templates/step.rb.erb +10 -2
  67. data/lib/generators/stall/checkout/wizard/templates/wizard.rb.erb +1 -1
  68. data/lib/generators/stall/install/templates/initializer.rb +85 -1
  69. data/lib/generators/stall/model/model_generator.rb +27 -0
  70. data/lib/generators/stall/service/service_generator.rb +39 -0
  71. data/lib/generators/stall/service/templates/service.rb.erb +16 -0
  72. data/lib/generators/stall/shipping/calculator/calculator_generator.rb +17 -0
  73. data/lib/generators/stall/shipping/calculator/templates/calculator.rb.erb +29 -0
  74. data/lib/stall.rb +4 -0
  75. data/lib/stall/addressable.rb +28 -2
  76. data/lib/stall/cart_helper.rb +84 -0
  77. data/lib/stall/carts_cleaner.rb +46 -0
  78. data/lib/stall/checkout.rb +2 -0
  79. data/lib/stall/checkout/informations_checkout_step.rb +3 -5
  80. data/lib/stall/checkout/payment_checkout_step.rb +3 -0
  81. data/lib/stall/checkout/payment_return_checkout_step.rb +11 -0
  82. data/lib/stall/checkout/shipping_method_checkout_step.rb +2 -1
  83. data/lib/stall/checkout/step.rb +47 -11
  84. data/lib/stall/checkout/step_form.rb +71 -0
  85. data/lib/stall/checkout/wizard.rb +15 -5
  86. data/lib/stall/config.rb +51 -0
  87. data/lib/stall/engine.rb +24 -2
  88. data/lib/stall/payable.rb +26 -0
  89. data/lib/stall/payments.rb +7 -3
  90. data/lib/stall/payments/config.rb +37 -0
  91. data/lib/stall/payments/fake_gateway_payment_notification.rb +34 -0
  92. data/lib/stall/payments/gateway.rb +19 -12
  93. data/lib/stall/payments/urls_config.rb +40 -0
  94. data/lib/stall/priceable.rb +7 -0
  95. data/lib/stall/rails/currency_helper.rb +1 -1
  96. data/lib/stall/routes.rb +17 -4
  97. data/lib/stall/sellable.rb +1 -2
  98. data/lib/stall/shipping.rb +3 -1
  99. data/lib/stall/shipping/calculator.rb +36 -3
  100. data/lib/stall/shipping/config.rb +17 -3
  101. data/lib/stall/shipping/country_weight_table_calculator.rb +1 -1
  102. data/lib/stall/shipping/free_shipping_calculator.rb +1 -1
  103. data/lib/stall/utils.rb +2 -2
  104. data/lib/stall/utils/config_dsl.rb +5 -1
  105. data/lib/stall/version.rb +1 -1
  106. data/lib/tasks/stall_tasks.rake +11 -0
  107. metadata +73 -15
  108. data/app/assets/javascripts/stall/application.js +0 -13
  109. data/app/assets/javascripts/stall/carts.js +0 -2
  110. data/app/helpers/stall/cart_helper.rb +0 -28
  111. data/app/models/stall/address.rb +0 -5
  112. data/app/models/stall/address_ownership.rb +0 -8
  113. data/app/models/stall/cart.rb +0 -31
  114. data/app/models/stall/customer.rb +0 -8
  115. data/app/models/stall/line_item.rb +0 -49
  116. data/app/models/stall/payment.rb +0 -14
  117. data/app/models/stall/payment_method.rb +0 -7
  118. data/app/models/stall/product_list.rb +0 -68
  119. data/app/models/stall/shipment.rb +0 -15
  120. data/app/models/stall/shipping_method.rb +0 -5
@@ -0,0 +1,16 @@
1
+ class <%= service_class_name %> < Stall::<%= service_class_name %>
2
+ # The call call method is the entry point for the service to process the data
3
+ # it was given at initialization
4
+ #
5
+ # Many overrides would want to call `super` to let the service do its
6
+ # classical job, then do something depending on the success or failure of
7
+ # the original work. This pattern is shown below.
8
+ #
9
+ # def call
10
+ # if super
11
+ # # Do something when it worked
12
+ # else
13
+ # # Do something when it didn't
14
+ # end
15
+ # end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Stall
2
+ module Shipping
3
+ class CalculatorGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def copy_calculator_template
7
+ template 'calculator.rb.erb', "lib/#{ file_path }.rb"
8
+ end
9
+
10
+ def register_calculator_in_initializer
11
+ insert_into_file "config/initializers/stall.rb", after: "Stall.configure do |config|\n" do
12
+ indent "config.shipping.register_calculator :#{ singular_name }, #{ class_name }\n"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ class <%= class_name %> < Stall::Shipping::Calculator
2
+ # Use this method to handle the related shipping method availibility depending
3
+ # on the cart data available to the calculator
4
+ #
5
+ def available?
6
+ true
7
+ end
8
+
9
+ # Write your logic here and return the price for the current cart
10
+ #
11
+ # You can access `cart` and `shipping_method` (the model) here.
12
+ #
13
+ def price
14
+ 0
15
+ end
16
+
17
+ # Allows you to override how calculator's eot_price price is calculated
18
+ #
19
+ # def eot_price
20
+ # price / (1 + (vat_rate / 100.0))
21
+ # end
22
+
23
+ # Allows you to override the VAT for the calculator which allows calculating
24
+ # the EOT price
25
+ #
26
+ # def vat_rate
27
+ # Stall.config.vat_rate
28
+ # end
29
+ end
@@ -5,6 +5,7 @@ require 'request_store'
5
5
  require 'haml-rails'
6
6
  require 'simple_form'
7
7
  require 'country_select'
8
+ require 'cocoon'
8
9
 
9
10
  require 'stall/rails/routing_mapper'
10
11
  require 'stall/rails/currency_helper'
@@ -15,12 +16,15 @@ module Stall
15
16
 
16
17
  autoload :Sellable
17
18
  autoload :Addressable
19
+ autoload :Priceable
20
+ autoload :Payable
18
21
 
19
22
  autoload :Checkout
20
23
  autoload :Shipping
21
24
  autoload :Payments
22
25
 
23
26
  autoload :Routes
27
+ autoload :CartHelper
24
28
  autoload :Config
25
29
  autoload :Utils
26
30
 
@@ -6,7 +6,9 @@ module Stall
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- has_many :address_ownerships, as: :addressable, dependent: :destroy
9
+ has_many :address_ownerships, as: :addressable, dependent: :destroy,
10
+ inverse_of: :addressable
11
+
10
12
  has_many :addresses, through: :address_ownerships
11
13
  accepts_nested_attributes_for :address_ownerships, allow_destroy: true
12
14
  end
@@ -26,7 +28,16 @@ module Stall
26
28
  define_method(:"#{ type }_address=") do |address|
27
29
  ownership = address_ownership_for(type) || address_ownerships.build(type => true)
28
30
  ownership.address = address
29
- ownership.save if persisted?
31
+ instance_variable_set(instance_variable_name, address)
32
+ end
33
+
34
+ define_method(:"build_#{ type }_address") do |attributes = {}|
35
+ if (ownership = address_ownership_for(type))
36
+ ownership.send(:"#{ type }=", false)
37
+ end
38
+
39
+ ownership = address_ownerships.build(type => true)
40
+ address = ownership.build_address(attributes)
30
41
  instance_variable_set(instance_variable_name, address)
31
42
  end
32
43
 
@@ -39,6 +50,21 @@ module Stall
39
50
  instance_variable_set(instance_variable_name, ownership.address)
40
51
  end
41
52
  end
53
+
54
+ define_method(:"#{ type }_address_attributes=") do |attributes|
55
+ attributes.delete(:id)
56
+
57
+ ownership = address_ownership_for(type) || address_ownerships.build(type => true)
58
+ address = ownership.address || ownership.build_address
59
+ address.assign_attributes(attributes)
60
+ instance_variable_set(instance_variable_name, address)
61
+ end
62
+
63
+ define_method(:"mark_address_ownership_as_#{ type }") do |ownership|
64
+ ownership.send(:"mark_as_#{ type }").tap do
65
+ instance_variable_set(instance_variable_name, ownership.address)
66
+ end
67
+ end
42
68
  end
43
69
  end
44
70
  end
@@ -0,0 +1,84 @@
1
+ module Stall
2
+ module CartHelper
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ if respond_to?(:helper_method)
7
+ helper_method :current_cart, :current_cart_key
8
+ end
9
+
10
+ if respond_to?(:after_action)
11
+ after_action :store_cart_to_cookies
12
+ end
13
+ end
14
+
15
+ def current_cart
16
+ RequestStore.store[cart_key] ||= load_current_cart
17
+ end
18
+
19
+ protected
20
+
21
+ def current_cart_key
22
+ params[:cart_key].try(:to_sym) || :default
23
+ end
24
+
25
+ def load_current_cart(identifier = current_cart_key)
26
+ if (cart = find_cart(identifier))
27
+ return prepare_cart(cart)
28
+ end
29
+
30
+ # If no token was stored or the token does not exist anymore, create a
31
+ # new cart and store the new token
32
+ #
33
+ prepare_cart(cart_class.new(identifier: identifier))
34
+ end
35
+
36
+ def prepare_cart(cart)
37
+ cart.tap do |cart|
38
+ # Keep track of potential customer locale switching to allow e-mailing
39
+ # him in his last used locale
40
+ cart.customer.locale = I18n.locale if cart.customer
41
+
42
+ # Only update locale change for existing carts. New carts don't need
43
+ # to be saved, avoiding each robot or simple visitors to create a
44
+ # cart on large shops.
45
+ cart.save unless cart.new_record?
46
+ end
47
+ end
48
+
49
+ def find_cart(identifier)
50
+ if (cart_token = cookies.encrypted[cart_key(identifier)])
51
+ if (current_cart = ProductList.find_by_token(cart_token)) && current_cart.active?
52
+ return current_cart
53
+ else
54
+ # Remove any cart that can't be fetched, either because it's already
55
+ # paid, or because it was cleaned out
56
+ remove_cart_from_cookies(identifier)
57
+ nil
58
+ end
59
+ end
60
+ end
61
+
62
+ def remove_cart_from_cookies(identifier = current_cart_key)
63
+ cookies.delete(cart_key(identifier))
64
+ end
65
+
66
+ def cart_key(identifier = current_cart_key)
67
+ ['stall', 'cart', identifier.to_s].join('.')
68
+ end
69
+
70
+ def cart_class
71
+ Cart
72
+ end
73
+
74
+ def store_cart_to_cookies
75
+ if current_cart.persisted?
76
+ store_cart_cookie_for(current_cart.identifier, current_cart)
77
+ end
78
+ end
79
+
80
+ def store_cart_cookie_for(identifier, cart)
81
+ cookies.encrypted.permanent[cart_key(identifier)] = cart.token
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ module Stall
2
+ class CartsCleaner
3
+ attr_reader :cart_model
4
+
5
+ def initialize(cart_model)
6
+ @cart_model = cart_model
7
+ end
8
+
9
+ def clean!
10
+ clean_empty_carts
11
+ clean_aborted_carts
12
+ end
13
+
14
+ private
15
+
16
+ # Empty carts are cleaned with a .delete_all call for optimization.
17
+ #
18
+ # Empty carts should not need to run destroy callbacks, not being related to
19
+ # any external model.
20
+ #
21
+ def clean_empty_carts
22
+ carts = cart_model.empty.older_than(Stall.config.empty_carts_expires_after.ago)
23
+
24
+ log "Cleaning #{ carts.count } empty carts ..."
25
+ carts.delete_all
26
+ log "Done."
27
+ end
28
+
29
+ # Unpaid carts have line items and other models related. Since empty carts
30
+ # are already cleaned, there should not be too many carts to destroy here
31
+ # and we can safely use .destroy_all to deeply clean carts.
32
+ #
33
+ # Note : The given cart model should implement the `.unpaid` method
34
+ def clean_aborted_carts
35
+ carts = cart_model.aborted(before: Stall.config.aborted_carts_expires_after.ago)
36
+
37
+ log "Cleaning #{ carts.count } aborted carts ..."
38
+ carts.destroy_all
39
+ log "Done."
40
+ end
41
+
42
+ def log(*args)
43
+ puts(*args) unless Rails.env.test?
44
+ end
45
+ end
46
+ end
@@ -4,11 +4,13 @@ module Stall
4
4
 
5
5
  autoload :Wizard
6
6
  autoload :Step
7
+ autoload :StepForm
7
8
 
8
9
  autoload :InformationsCheckoutStep
9
10
  autoload :ShippingMethodCheckoutStep
10
11
  autoload :PaymentMethodCheckoutStep
11
12
  autoload :PaymentCheckoutStep
13
+ autoload :PaymentReturnCheckoutStep
12
14
 
13
15
  class WizardNotFoundError < StandardError; end
14
16
  end
@@ -10,7 +10,7 @@ module Stall
10
10
  def process
11
11
  cart.assign_attributes(cart_params)
12
12
  process_addresses
13
- cart.save!
13
+ cart.save if valid?
14
14
  end
15
15
 
16
16
  private
@@ -20,10 +20,8 @@ module Stall
20
20
  end
21
21
 
22
22
  def ensure_address(type)
23
- unless cart.address_ownership_for(type)
24
- ownership = cart.address_ownerships.build(type => true)
25
- ownership.build_address
26
- end
23
+ ownership = cart.address_ownership_for(type) || cart.address_ownerships.build(type => true)
24
+ ownership.address || ownership.build_address
27
25
  end
28
26
 
29
27
  def process_addresses
@@ -1,6 +1,9 @@
1
1
  module Stall
2
2
  module Checkout
3
3
  class PaymentCheckoutStep < Stall::Checkout::Step
4
+ def process
5
+ return true if params[:succeeded]
6
+ end
4
7
  end
5
8
  end
6
9
  end
@@ -0,0 +1,11 @@
1
+ module Stall
2
+ module Checkout
3
+ class PaymentReturnCheckoutStep < Stall::Checkout::Step
4
+ include Stall::CartHelper
5
+
6
+ def prepare
7
+ remove_cart_from_cookies(cart.identifier)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -13,7 +13,8 @@ module Stall
13
13
  private
14
14
 
15
15
  def calculate_shipping_fee!
16
- Stall::ShippingFeeCalculatorService.new(cart).call
16
+ service_class = Stall.config.service_for(:shipping_fee_calculator)
17
+ service_class.new(cart).call
17
18
  end
18
19
  end
19
20
  end
@@ -3,11 +3,10 @@ module Stall
3
3
  class StepNotFoundError < StandardError; end
4
4
 
5
5
  class Step
6
- attr_reader :cart, :params
6
+ attr_reader :cart
7
7
 
8
- def initialize(cart, params)
8
+ def initialize(cart)
9
9
  @cart = cart
10
- @params = params
11
10
  end
12
11
 
13
12
  # Allow injecting dependencies on step initialization and accessing
@@ -25,32 +24,69 @@ module Stall
25
24
  end
26
25
 
27
26
  def process
28
- cart.update_attributes(cart_params)
27
+ save
29
28
  end
30
29
 
31
30
  def cart_params
32
- @cart_params ||= params.require(:cart).permit!
31
+ @cart_params ||= if params[:cart]
32
+ params.require(:cart).permit!
33
+ else
34
+ {}.with_indifferent_access
35
+ end
33
36
  end
34
37
 
35
38
  def skip?
36
39
  false
37
40
  end
38
41
 
42
+ def is?(key)
43
+ identifier == key
44
+ end
45
+
46
+ def identifier
47
+ @identifier ||= begin
48
+ class_name = self.class.name.demodulize
49
+ class_name.gsub(/CheckoutStep$/, '').underscore.to_sym
50
+ end
51
+ end
52
+
53
+ # Abstracts the simple case of assigning the submitted parameters to the
54
+ # cart object, running the step validations and saving the cart
55
+ def save
56
+ cart.assign_attributes(cart_params)
57
+ cart.save if valid?
58
+ end
59
+
39
60
  # Handles conversion from an identifier to a checkout step class, allowing
40
61
  # us to specify a list of symbols in our wizard's .step macro
41
62
  #
42
63
  def self.for(identifier)
43
64
  name = identifier.to_s.camelize
44
65
  step_name = [name, 'CheckoutStep'].join
66
+ # Try loading step from app
67
+ step = Stall::Utils.try_load_constant(step_name)
68
+ # Try loading step from stall core or lib if not found in app
69
+ step = Stall::Utils.try_load_constant(
70
+ ['Stall', 'Checkout', step_name.demodulize].join('::')
71
+ ) unless step
72
+
73
+ unless step
74
+ raise StepNotFoundError,
75
+ "No checkout step was found for #{ identifier }. You can generate " +
76
+ "it with `rails g stall:checkout:step #{ identifier }`"
77
+ end
45
78
 
46
- step = Stall::Utils.try_load_constant(step_name) ||
47
- Stall::Utils.try_load_constant(['Stall', 'Checkout', step_name.demodulize].join('::'))
79
+ step
80
+ end
48
81
 
49
- return step if step
82
+ def self.validations(&block)
83
+ return @validations unless block
84
+ @validations = Stall::Checkout::StepForm.build(&block)
85
+ end
50
86
 
51
- raise StepNotFoundError,
52
- "No checkout step was found for #{ identifier }. You can generate " +
53
- "it with `rails g stall:checkout:step #{ identifier }`"
87
+ def valid?
88
+ return true unless (validations = self.class.validations)
89
+ validations.new(cart, self).validate
54
90
  end
55
91
  end
56
92
  end
@@ -0,0 +1,71 @@
1
+ module Stall
2
+ module Checkout
3
+ class StepForm
4
+ include ActiveModel::Validations
5
+
6
+ class_attribute :nested_forms
7
+
8
+ attr_reader :object, :step
9
+
10
+ delegate :errors, to: :object
11
+
12
+ def initialize(object, step)
13
+ @object = object
14
+ @step = step
15
+ end
16
+
17
+ def validate
18
+ super && validate_nested_forms
19
+ end
20
+
21
+ def self.nested(type, &block)
22
+ self.nested_forms ||= {}
23
+ nested_forms[type] = build(&block)
24
+ end
25
+
26
+ def self.build(&block)
27
+ Class.new(StepForm, &block)
28
+ end
29
+
30
+ def method_missing(method, *args, &block)
31
+ if object.respond_to?(method, true)
32
+ object.send(method, *args, &block)
33
+ elsif step.respond_to?(method, true)
34
+ step.send(method, *args, &block)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ # Override model name instanciation to add a name, since the form classes
41
+ # are anonymous, and ActiveModel::Name does not support unnamed classes
42
+ def model_name
43
+ @model_name ||= ActiveModel::Name.new(self, nil, object.class.name)
44
+ end
45
+
46
+ private
47
+
48
+ # Validates all registered nested forms
49
+ #
50
+ # Note : We use `forms.map.all?` instead if `forms.all?` to ensure
51
+ # all the validations are called and the iteration does not stop as soon
52
+ # as a validation fails
53
+ #
54
+ def validate_nested_forms
55
+ # If no nested forms are present in the class, just return true since
56
+ # no validation should be tested
57
+ return true unless self.class.nested_forms
58
+
59
+ # Run all validations on all nested forms and ensure they're all valid
60
+ self.class.nested_forms.map do |name, form|
61
+ if object.respond_to?(name) && (model = object.send(name))
62
+ Array.wrap(model).map { |m| form.new(m, step).validate }.all?
63
+ else
64
+ # Nested validations shouldn't be run on undefined relations
65
+ true
66
+ end
67
+ end.all?
68
+ end
69
+ end
70
+ end
71
+ end