stall 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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