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
@@ -19,15 +19,16 @@ module Stall
19
19
  @cart = cart
20
20
  end
21
21
 
22
- def initialize_current_step(params, &block)
23
- step = current_step.new(cart, params)
24
- # This block allows us to let the config inject controller-bound
25
- # dependencies to the step just after it is initialized
22
+ def initialize_current_step(&block)
23
+ step = current_step.new(cart)
24
+
25
+ # This block allows us to let inject controller-bound dependencies
26
+ # into the step just after it's initialized
26
27
  block.call(step) if block
27
28
 
28
29
  if step.skip?
29
30
  validate_current_step!
30
- initialize_current_step(params, &block)
31
+ initialize_current_step(&block)
31
32
  else
32
33
  step
33
34
  end
@@ -56,6 +57,10 @@ module Stall
56
57
  step_index && step_index >= steps_count - 1
57
58
  end
58
59
 
60
+ def step_complete?(step_identifier)
61
+ steps.index(cart.state) > steps.index(step_identifier.to_sym)
62
+ end
63
+
59
64
  def steps_count
60
65
  @steps_count ||= steps.length
61
66
  end
@@ -65,6 +70,11 @@ module Stall
65
70
  cart.save!
66
71
  end
67
72
 
73
+ def move_to_step!(target_step)
74
+ cart.state = target_step
75
+ cart.save!
76
+ end
77
+
68
78
  def self.route_key
69
79
  name.gsub(/CheckoutWizard/, '').underscore.gsub('_', ' ').parameterize
70
80
  end
@@ -1,6 +1,13 @@
1
1
  module Stall
2
2
  class Config
3
3
  extend Stall::Utils::ConfigDSL
4
+ param :store_name
5
+
6
+ # Admin e-mail address to which order notifications will be sent
7
+ param :admin_email, -> { ENV['STALL_ADMIN_EMAIL'] || 'admin.change_me_in.stall.rb@example.com' }
8
+
9
+ # E-mail address used to send e-mails to customers
10
+ param :sender_email, -> { ENV['STALL_SENDER_EMAIL'] || 'shop.change_me_in.stall.rb@example.com' }
4
11
 
5
12
  # Default VAT rate
6
13
  param :vat_rate, BigDecimal.new('20.0')
@@ -11,9 +18,17 @@ module Stall
11
18
  # Engine's ApplicationController parent
12
19
  param :application_controller_ancestor, '::ApplicationController'
13
20
 
21
+ param :mailers_parent_class, 'ActionMailer::Base'
22
+
23
+ # Default layout used for the checkout
24
+ param :default_layout
25
+
14
26
  # Default currency for money objects
15
27
  param :default_currency, 'EUR'
16
28
 
29
+ # Default app domain for building routes
30
+ param :_default_app_domain
31
+
17
32
  # Default checkout wizard used
18
33
  param :default_checkout_wizard, 'DefaultCheckoutWizard'
19
34
 
@@ -23,10 +38,23 @@ module Stall
23
38
  # Default step initialization hook
24
39
  param :_steps_initialization_callback
25
40
 
41
+ param :services, {}
42
+
43
+ # Duration after which an empty cart is cleaned out by the rake task
44
+ param :empty_carts_expires_after, 1.day
45
+
46
+ # Duration after which an aborted is cleaned out by the rake task
47
+ param :aborted_carts_expires_after, 14.days
48
+
49
+
26
50
  def shipping
27
51
  @shipping ||= Stall::Shipping::Config.new
28
52
  end
29
53
 
54
+ def payment
55
+ @payment ||= Stall::Payments::Config.new
56
+ end
57
+
30
58
  def steps_initialization(value = nil, &block)
31
59
  if (value ||= block)
32
60
  @_steps_initialization_callback = value
@@ -34,5 +62,28 @@ module Stall
34
62
  @_steps_initialization_callback
35
63
  end
36
64
  end
65
+
66
+ def default_app_domain
67
+ _default_app_domain || ENV['APP_DOMAIN']
68
+ end
69
+
70
+ # Fetch user config and add top-namespace lookup to avoid collision
71
+ # with Stall module services
72
+ #
73
+ # Default allows looking up Stall namespace automatically, when no
74
+ # config has been given
75
+ def service_for(identifier)
76
+ class_name = if (service_name = services[identifier])
77
+ "::#{ services[identifier].gsub(/^::/, '') }"
78
+ else
79
+ "Stall::#{ identifier.to_s.camelize }Service"
80
+ end
81
+
82
+ class_name.constantize
83
+ end
84
+
85
+ def services=(value)
86
+ self.services.merge!(value)
87
+ end
37
88
  end
38
89
  end
@@ -1,15 +1,37 @@
1
1
  module Stall
2
2
  class Engine < ::Rails::Engine
3
- initializer 'set money gem default currency' do
3
+ initializer 'stall.set_money_gem_default_currency' do
4
4
  Money.default_currency = Stall.config.default_currency
5
5
  end
6
6
 
7
- initializer 'override actionview number helpers' do
7
+ initializer 'stall.override_actionview_number_helpers' do
8
8
  ActiveSupport.on_load(:action_view) do
9
9
  include Stall::CurrencyHelper
10
10
  end
11
11
  end
12
12
 
13
+ initializer 'stall.include_cart_helper' do
14
+ ActiveSupport.on_load(:action_controller) do
15
+ include Stall::CartHelper
16
+ end
17
+ end
18
+
19
+ initializer 'stall.ensure_shipping_method_for_all_calculators' do
20
+ Stall::Shipping.calculators.each_key do |name|
21
+ ShippingMethod.where(identifier: name).first_or_create do |method|
22
+ method.name = name.to_s.humanize
23
+ end
24
+ end
25
+ end
26
+
27
+ initializer 'stall.ensure_payment_method_for_all_gateways' do
28
+ Stall::Payments.gateways.each_key do |name|
29
+ PaymentMethod.where(identifier: name).first_or_create do |method|
30
+ method.name = name.to_s.humanize
31
+ end
32
+ end
33
+ end
34
+
13
35
  # Development : Configure rails generators to only generate the target
14
36
  # files and not try to generate useless complementary files
15
37
  #
@@ -0,0 +1,26 @@
1
+ module Stall
2
+ module Payable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one :payment, dependent: :destroy,
7
+ inverse_of: :cart,
8
+ foreign_key: :cart_id
9
+ accepts_nested_attributes_for :payment
10
+
11
+ scope :paid, -> {
12
+ joins(:payment).where.not(stall_payments: { paid_at: nil })
13
+ }
14
+
15
+ scope :finalized, -> { paid }
16
+
17
+ scope :aborted, ->(options = {}) {
18
+ joins('LEFT JOIN stall_payments ON stall_payments.cart_id = stall_product_lists.id')
19
+ .where(stall_payments: { paid_at: nil })
20
+ .older_than(options.fetch(:before, 1.day.ago))
21
+ }
22
+
23
+ delegate :paid?, to: :payment, allow_nil: true
24
+ end
25
+ end
26
+ end
@@ -3,11 +3,15 @@ module Stall
3
3
  extend ActiveSupport::Autoload
4
4
 
5
5
  autoload :Gateway
6
-
7
- # autoload :Config
6
+ autoload :Config
7
+ autoload :UrlsConfig
8
+ autoload :FakeGatewayPaymentNotification
8
9
 
9
10
  mattr_reader :gateways
10
- @@gateways = {}
11
+ @@gateways = {}.with_indifferent_access
12
+
13
+ mattr_reader :payment_urls
14
+ @@payment_urls = {}.with_indifferent_access
11
15
  end
12
16
  end
13
17
 
@@ -0,0 +1,37 @@
1
+ module Stall
2
+ module Payments
3
+ class Config
4
+ extend Stall::Utils::ConfigDSL
5
+
6
+ # This mehtod allows for registering an in-app gateway that can be
7
+ # auto loaded without having to explicitly require it
8
+ #
9
+ # Use a string representing the gateway name if the target class is
10
+ # auto-loaded by Rails
11
+ #
12
+ def register_gateway(name, gateway)
13
+ Stall::Payments.gateways[name] = gateway
14
+ end
15
+
16
+ def configure
17
+ yield self
18
+ end
19
+
20
+ def configure_urls(&block)
21
+ Stall::Payments::UrlsConfig.config_block = block
22
+ end
23
+
24
+ def method_missing(name, *args)
25
+ if (gateway = Stall::Payments::Gateway.for(name))
26
+ if block_given?
27
+ yield gateway
28
+ else
29
+ gateway
30
+ end
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Stall
2
+ module Payments
3
+ class FakeGatewayPaymentNotification
4
+ attr_reader :cart
5
+
6
+ def initialize(cart)
7
+ @cart = cart
8
+ end
9
+
10
+ def params
11
+ raise 'FakeGatewayPaymentNotification subclasses must define the ' \
12
+ '#params method to return a valid notification params hash.'
13
+ end
14
+
15
+ def raw_post
16
+ params.to_query
17
+ end
18
+
19
+ private
20
+
21
+ def transaction_id
22
+ ['FAKE', cart.reference, 0].join('-')
23
+ end
24
+
25
+ def gateway
26
+ @gateway ||= gateway_class.new(cart)
27
+ end
28
+
29
+ def gateway_class
30
+ @gateway_class ||= Stall::Payments::Gateway.for(cart.payment.payment_method)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -10,24 +10,27 @@ module Stall
10
10
  end
11
11
 
12
12
  def self.register(name)
13
- Stall::Payments.gateways[name] = self
13
+ Stall.config.payment.register_gateway(name, self)
14
14
  end
15
15
 
16
- def self.cart_id_from(_request)
17
- raise NoMethodError,
18
- 'Subclasses must implement the .cart_id_from(request) class method ' \
19
- 'to allow retrieving the cart from the remote gateway notification ' \
20
- 'request object'
16
+ def self.for(payment_method)
17
+ identifier = case payment_method
18
+ when String, Symbol then payment_method.to_s
19
+ else payment_method.identifier
20
+ end
21
+
22
+ gateway = Stall::Payments.gateways[identifier]
23
+ String === gateway ? gateway.constantize : gateway
21
24
  end
22
25
 
23
- def self.cart_id_from_transaction_id(transaction_id)
24
- transaction_id && transaction_id.split('-')[2].to_i
26
+ def self.request(cart)
27
+ raise NoMethodError,
28
+ 'Subclasses must implement the .request(cart) class method '
25
29
  end
26
30
 
27
- def process_payment_for(_request)
31
+ def self.response(_request)
28
32
  raise NoMethodError,
29
- 'Subclasses must implement the #process_payment_for(request) ' \
30
- 'method to handle payment verifications and cart payment validation'
33
+ 'Subclasses must implement the .response(request) class method '
31
34
  end
32
35
 
33
36
  def transaction_id
@@ -50,10 +53,14 @@ module Stall
50
53
  { text: nil }
51
54
  end
52
55
 
56
+ def payment_urls
57
+ @payment_urls ||= Stall::Payments::UrlsConfig.new(cart)
58
+ end
59
+
53
60
  private
54
61
 
55
62
  def next_transaction_id
56
- if (last_transaction = Stall::Payment.order("data->>'transaction_id' DESC").select(:data).first)
63
+ if (last_transaction = Payment.order("data->>'transaction_id' DESC").select(:data).first)
57
64
  if (id = last_transaction.transaction_id)
58
65
  index = id.split('-').pop.to_i + 1
59
66
  return transaction_id_for(index)
@@ -0,0 +1,40 @@
1
+ module Stall
2
+ module Payments
3
+ class UrlsConfig
4
+ include Rails.application.routes.url_helpers
5
+
6
+ class_attribute :config_block
7
+
8
+ attr_reader :cart
9
+
10
+ attr_accessor :payment_notification_url,
11
+ :payment_success_return_url,
12
+ :payment_failure_return_url
13
+
14
+ def initialize(cart)
15
+ @cart = cart
16
+
17
+ # Parse URLs
18
+ instance_exec(self, &config_block)
19
+ end
20
+
21
+ private
22
+
23
+ def config_block
24
+ self.class.config_block || default_config
25
+ end
26
+
27
+ def default_config
28
+ ->(urls) {
29
+ urls.payment_notification_url = notify_payment_url(gateway: gateway_identifier, host: Stall.config.default_app_domain)
30
+ urls.payment_success_return_url = process_checkout_step_url(cart.identifier, host: Stall.config.default_app_domain)
31
+ urls.payment_failure_return_url = process_checkout_step_url(cart.identifier, host: Stall.config.default_app_domain)
32
+ }
33
+ end
34
+
35
+ def gateway_identifier
36
+ @gateway_identifier ||= cart.payment.payment_method.identifier
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module Stall
2
+ module Priceable
3
+ def vat
4
+ price - eot_price
5
+ end
6
+ end
7
+ end
@@ -18,7 +18,7 @@ module Stall
18
18
  options[:unit] = price.symbol
19
19
  end
20
20
 
21
- original_number_to_currency(price, options)
21
+ original_number_to_currency(price.to_f, options)
22
22
  end
23
23
  end
24
24
  end
@@ -13,13 +13,26 @@ module Stall
13
13
  resources :line_items
14
14
  end
15
15
 
16
- resources :checkouts, only: [:show]
16
+ get 'checkout/:cart_key' => 'checkouts#show', as: :checkout
17
17
 
18
- scope '/checkout/:type/:cart_id', module: 'checkout', as: :checkout do
19
- resource :step, only: [:show, :update]
18
+ scope 'checkout', module: 'checkout', as: :checkout do
19
+ scope '(:cart_key)' do
20
+ resource :step, only: [:show, :update] do
21
+ post '/', action: :update, as: :update
22
+ get '/process', action: :update, as: :process
23
+ get 'change/:step', action: :change, as: :change
24
+ end
25
+ end
20
26
  end
21
27
 
22
- match '/payments/:gateway/process' => 'payments#process', via: [:get, :post]
28
+
29
+ scope '/:gateway' do
30
+ resource :payment, only: [] do
31
+ member do
32
+ match 'notify', action: 'notify', via: [:get, :post]
33
+ end
34
+ end
35
+ end
23
36
  end
24
37
  end
25
38
  end
@@ -3,8 +3,7 @@ module Stall
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- has_many :line_items, class_name: 'Stall::LineItem',
7
- dependent: :nullify,
6
+ has_many :line_items, dependent: :nullify,
8
7
  as: :sellable,
9
8
  inverse_of: :sellable
10
9
  end
@@ -10,7 +10,9 @@ module Stall
10
10
  autoload :Config
11
11
 
12
12
  mattr_reader :calculators
13
- @@calculators = {}
13
+ @@calculators = {}.with_indifferent_access
14
+
15
+ class CalculatorNotFound < StandardError; end
14
16
  end
15
17
  end
16
18