stall 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +7 -0
- data/Rakefile +22 -0
- data/app/assets/javascripts/stall.coffee +12 -0
- data/app/assets/javascripts/stall/add-to-cart-form.coffee +22 -0
- data/app/assets/javascripts/stall/application.js +13 -0
- data/app/assets/javascripts/stall/carts.js +2 -0
- data/app/assets/stylesheets/stall/application.css +15 -0
- data/app/assets/stylesheets/stall/carts.css +4 -0
- data/app/controllers/stall/application_controller.rb +18 -0
- data/app/controllers/stall/carts_controller.rb +28 -0
- data/app/controllers/stall/checkout/steps_controller.rb +32 -0
- data/app/controllers/stall/checkouts_controller.rb +9 -0
- data/app/controllers/stall/line_items_controller.rb +26 -0
- data/app/helpers/stall/add_to_cart_helper.rb +10 -0
- data/app/helpers/stall/application_helper.rb +4 -0
- data/app/helpers/stall/cart_helper.rb +28 -0
- data/app/helpers/stall/checkout_helper.rb +7 -0
- data/app/models/stall.rb +5 -0
- data/app/models/stall/address.rb +5 -0
- data/app/models/stall/address_ownership.rb +15 -0
- data/app/models/stall/cart.rb +14 -0
- data/app/models/stall/customer.rb +5 -0
- data/app/models/stall/line_item.rb +45 -0
- data/app/models/stall/product_list.rb +56 -0
- data/app/models/stall/shipment.rb +15 -0
- data/app/models/stall/shipping_method.rb +5 -0
- data/app/services/stall/add_to_cart_service.rb +52 -0
- data/app/services/stall/base_service.rb +4 -0
- data/app/services/stall/shipping_fee_calculator_service.rb +21 -0
- data/app/views/checkout/steps/_informations.html.haml +41 -0
- data/app/views/checkout/steps/_payment_method.html.haml +1 -0
- data/app/views/checkout/steps/_shipping_method.html.haml +10 -0
- data/app/views/layouts/stall/application.html.erb +15 -0
- data/app/views/stall/carts/show.html.haml +23 -0
- data/app/views/stall/checkout/steps/show.html.haml +1 -0
- data/app/views/stall/line_items/_add_error.html.haml +17 -0
- data/app/views/stall/line_items/_added.html.haml +23 -0
- data/app/views/stall/line_items/_form.html.haml +12 -0
- data/config/locales/stall.en.yml +60 -0
- data/config/locales/stall.fr.yml +60 -0
- data/db/migrate/20151103154313_create_stall_line_items.rb +19 -0
- data/db/migrate/20160118121116_create_stall_product_lists.rb +15 -0
- data/db/migrate/20160118124016_create_customers.rb +11 -0
- data/db/migrate/20160122143746_create_stall_addresses.rb +17 -0
- data/db/migrate/20160122143748_create_stall_address_ownerships.rb +17 -0
- data/db/migrate/20160124014144_create_stall_shipping_methods.rb +10 -0
- data/db/migrate/20160124020313_create_stall_shipments.rb +17 -0
- data/lib/generators/stall/checkout/step/step_generator.rb +20 -0
- data/lib/generators/stall/checkout/step/templates/step.rb.erb +2 -0
- data/lib/generators/stall/checkout/wizard/templates/wizard.rb.erb +3 -0
- data/lib/generators/stall/checkout/wizard/wizard_generator.rb +20 -0
- data/lib/generators/stall/install/install_generator.rb +17 -0
- data/lib/generators/stall/install/templates/initializer.rb +47 -0
- data/lib/stall.rb +38 -0
- data/lib/stall/addressable.rb +44 -0
- data/lib/stall/checkout.rb +15 -0
- data/lib/stall/checkout/informations_checkout_step.rb +41 -0
- data/lib/stall/checkout/payment_checkout_step.rb +6 -0
- data/lib/stall/checkout/payment_method_checkout_step.rb +6 -0
- data/lib/stall/checkout/shipping_method_checkout_step.rb +20 -0
- data/lib/stall/checkout/step.rb +47 -0
- data/lib/stall/checkout/wizard.rb +85 -0
- data/lib/stall/config.rb +27 -0
- data/lib/stall/engine.rb +30 -0
- data/lib/stall/rails/currency_helper.rb +26 -0
- data/lib/stall/rails/routing_mapper.rb +13 -0
- data/lib/stall/routes.rb +27 -0
- data/lib/stall/sellable.rb +8 -0
- data/lib/stall/sellable/mixin.rb +17 -0
- data/lib/stall/sellable/model.rb +46 -0
- data/lib/stall/shipping.rb +16 -0
- data/lib/stall/shipping/calculator.rb +28 -0
- data/lib/stall/shipping/config.rb +17 -0
- data/lib/stall/shipping/country_weight_table_calculator.rb +69 -0
- data/lib/stall/shipping/free_shipping_calculator.rb +23 -0
- data/lib/stall/utils.rb +22 -0
- data/lib/stall/utils/config_dsl.rb +19 -0
- data/lib/stall/version.rb +3 -0
- data/lib/tasks/stall_tasks.rake +4 -0
- metadata +362 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateStallAddressOwnerships < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :stall_address_ownerships do |t|
|
4
|
+
t.references :address, index: true
|
5
|
+
t.references :addressable, polymorphic: true
|
6
|
+
t.boolean :billing, default: false
|
7
|
+
t.boolean :shipping, default: false
|
8
|
+
|
9
|
+
t.timestamps null: false
|
10
|
+
|
11
|
+
t.index [:addressable_id, :addressable_type],
|
12
|
+
name: :index_address_ownerships_on_addressable_type_and_id
|
13
|
+
|
14
|
+
t.foreign_key :stall_addresses, column: :address_id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateStallShipments < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :stall_shipments do |t|
|
4
|
+
t.references :cart, index: true
|
5
|
+
t.references :shipping_method, index: true
|
6
|
+
t.monetize :price, currency: { present: false }
|
7
|
+
t.monetize :eot_price, currency: { present: false }
|
8
|
+
t.decimal :vat_rate
|
9
|
+
t.datetime :sent_at
|
10
|
+
|
11
|
+
t.timestamps null: false
|
12
|
+
end
|
13
|
+
|
14
|
+
add_foreign_key :stall_shipments, :stall_product_lists, column: :cart_id
|
15
|
+
add_foreign_key :stall_shipments, :stall_shipping_methods, column: :shipping_method_id
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class StepGenerator < ::Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def copy_template
|
7
|
+
template 'step.rb.erb', "lib/#{ file_path }.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Override provided file name to include checkout_wizard at the end and
|
13
|
+
# allow every NamedBase helpers to use that name
|
14
|
+
#
|
15
|
+
def file_name
|
16
|
+
@_file_name ||= [@file_name, 'checkout_step'].join('_')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class WizardGenerator < ::Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def copy_template
|
7
|
+
template 'wizard.rb.erb', "lib/#{ file_path }.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Override provided file name to include checkout_wizard at the end and
|
13
|
+
# allow every NamedBase helpers to use that name
|
14
|
+
#
|
15
|
+
def file_name
|
16
|
+
@_file_name ||= [@file_name, 'checkout_wizard'].join('_')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Stall
|
2
|
+
class InstallGenerator < ::Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def copy_migrations
|
6
|
+
rake 'stall_engine:install:migrations'
|
7
|
+
end
|
8
|
+
|
9
|
+
def copy_initializer
|
10
|
+
copy_file 'initializer.rb', 'config/initializers/stall.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
def copy_default_checkout_wizard
|
14
|
+
generate 'stall:wizard', 'default'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Stall.configure do |config|
|
2
|
+
# Global default VAT rate, can be overrided by products
|
3
|
+
#
|
4
|
+
# config.vat_rate = BigDecimal.new('20.0')
|
5
|
+
|
6
|
+
# Defines the default number of decimals precision used in prices
|
7
|
+
#
|
8
|
+
# config.prices_precision = 2
|
9
|
+
|
10
|
+
# Default engine's controllers ancestor class
|
11
|
+
#
|
12
|
+
# config.application_controller_ancestor = '::ApplicationController'
|
13
|
+
|
14
|
+
# Default currency used in the app for money objects.
|
15
|
+
#
|
16
|
+
# Note : If you need multi currency support in your app, you'll have to ensure
|
17
|
+
# that you create your line items, carts and orders with the right currency
|
18
|
+
# set.
|
19
|
+
#
|
20
|
+
# param :default_currency, 'EUR'
|
21
|
+
|
22
|
+
# Defined the default checkout wizard used for checking carts out in the
|
23
|
+
# shop process.
|
24
|
+
#
|
25
|
+
# Checkout wizards are used to customize the checkout steps.
|
26
|
+
#
|
27
|
+
# A default one is generated on stall's installation and can be found in your
|
28
|
+
# app at : lib/default_checkout_wizard.rb
|
29
|
+
#
|
30
|
+
# config.default_checkout_wizard = 'DefaultCheckoutWizard'
|
31
|
+
|
32
|
+
# Default line items weight when added to cart and no weight is found on
|
33
|
+
# the sellable
|
34
|
+
#
|
35
|
+
# This allows for simply setting a weight here and not in each product
|
36
|
+
# to calculate shipping fees depending on the total cart weight
|
37
|
+
#
|
38
|
+
# config.default_product_weight = 0
|
39
|
+
|
40
|
+
# Allows configuring which countries are available for free shipping offers
|
41
|
+
#
|
42
|
+
# Accepts an array of country codes or a proc with a Stall::Address argument
|
43
|
+
#
|
44
|
+
# Defaults to nil, which means all countries are available
|
45
|
+
#
|
46
|
+
# config.shipping.free_shipping.available = nil
|
47
|
+
end
|
data/lib/stall.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'has_secure_token'
|
2
|
+
require 'vertebra'
|
3
|
+
require 'money-rails'
|
4
|
+
require 'request_store'
|
5
|
+
require 'haml-rails'
|
6
|
+
require 'simple_form'
|
7
|
+
require 'country_select'
|
8
|
+
|
9
|
+
require 'stall/rails/routing_mapper'
|
10
|
+
require 'stall/rails/currency_helper'
|
11
|
+
require 'stall/engine'
|
12
|
+
|
13
|
+
module Stall
|
14
|
+
extend ActiveSupport::Autoload
|
15
|
+
|
16
|
+
autoload :Sellable
|
17
|
+
autoload :Addressable
|
18
|
+
|
19
|
+
autoload :Checkout
|
20
|
+
autoload :Shipping
|
21
|
+
|
22
|
+
autoload :Routes
|
23
|
+
autoload :Config
|
24
|
+
autoload :Utils
|
25
|
+
|
26
|
+
def self.table_name_prefix
|
27
|
+
'stall_'
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.config
|
31
|
+
@config ||= Stall::Config.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configure
|
35
|
+
yield config
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Provide the common behavior for all models that need to own addresses,
|
2
|
+
# for billing and shipping
|
3
|
+
#
|
4
|
+
module Stall
|
5
|
+
module Addressable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_many :address_ownerships, as: :addressable, dependent: :destroy
|
10
|
+
has_many :addresses, through: :address_ownerships
|
11
|
+
accepts_nested_attributes_for :address_ownerships, allow_destroy: true
|
12
|
+
end
|
13
|
+
|
14
|
+
def address_ownership_for(type)
|
15
|
+
address_ownerships.find do |ownership|
|
16
|
+
ownership.public_send(type)
|
17
|
+
end if [:shipping, :billing].include?(type.to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Provide billing and shipping address fetching and assigning shortcuts by
|
21
|
+
# using the address ownerships
|
22
|
+
#
|
23
|
+
[:shipping, :billing].each do |type|
|
24
|
+
instance_variable_name = :"@#{ type }_address"
|
25
|
+
|
26
|
+
define_method(:"#{ type }_address=") do |address|
|
27
|
+
ownership = address_ownership_for(type) || address_ownerships.build(type => true)
|
28
|
+
ownership.address = address
|
29
|
+
ownership.save if persisted?
|
30
|
+
instance_variable_set(instance_variable_name, address)
|
31
|
+
end
|
32
|
+
|
33
|
+
define_method(:"#{ type }_address") do
|
34
|
+
if (address = instance_variable_get(instance_variable_name))
|
35
|
+
return address
|
36
|
+
end
|
37
|
+
|
38
|
+
if (ownership = address_ownership_for(type))
|
39
|
+
instance_variable_set(instance_variable_name, ownership.address)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
autoload :Wizard
|
6
|
+
autoload :Step
|
7
|
+
|
8
|
+
autoload :InformationsCheckoutStep
|
9
|
+
autoload :ShippingMethodCheckoutStep
|
10
|
+
autoload :PaymentMethodCheckoutStep
|
11
|
+
autoload :PaymentCheckoutStep
|
12
|
+
|
13
|
+
class WizardNotFoundError < StandardError; end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class InformationsCheckoutStep < Stall::Checkout::Step
|
4
|
+
def prepare
|
5
|
+
ensure_customer
|
6
|
+
ensure_address(:billing)
|
7
|
+
ensure_address(:shipping)
|
8
|
+
end
|
9
|
+
|
10
|
+
def process
|
11
|
+
cart.assign_attributes(cart_params)
|
12
|
+
process_addresses
|
13
|
+
cart.save!
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def ensure_customer
|
19
|
+
cart.build_customer unless cart.customer
|
20
|
+
end
|
21
|
+
|
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
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_addresses
|
30
|
+
unless params[:use_another_address_for_billing]
|
31
|
+
# Remove submitted billing address
|
32
|
+
billing_ownership = cart.address_ownership_for(:billing)
|
33
|
+
cart.address_ownerships.destroy(billing_ownership)
|
34
|
+
# Set shipping address as the billing one
|
35
|
+
shipping_ownership = cart.address_ownership_for(:shipping)
|
36
|
+
shipping_ownership.billing = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class ShippingMethodCheckoutStep < Stall::Checkout::Step
|
4
|
+
def prepare
|
5
|
+
cart.build_shipment unless cart.shipment
|
6
|
+
end
|
7
|
+
|
8
|
+
def process
|
9
|
+
super
|
10
|
+
calculate_shipping_fee!
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def calculate_shipping_fee!
|
16
|
+
Stall::ShippingFeeCalculatorService.new(cart).call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class StepNotFoundError < StandardError; end
|
4
|
+
|
5
|
+
class Step
|
6
|
+
attr_reader :cart, :params
|
7
|
+
|
8
|
+
def initialize(cart, params)
|
9
|
+
@cart = cart
|
10
|
+
@params = params
|
11
|
+
end
|
12
|
+
|
13
|
+
# Allows for preparing to the cart for the current step before rendering
|
14
|
+
# the step's view
|
15
|
+
#
|
16
|
+
# Note : Meant to be overriden by subclasses
|
17
|
+
#
|
18
|
+
def prepare
|
19
|
+
end
|
20
|
+
|
21
|
+
def process
|
22
|
+
cart.update_attributes(cart_params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def cart_params
|
26
|
+
params.require(:cart).permit!
|
27
|
+
end
|
28
|
+
|
29
|
+
# Handles conversion from an identifier to a checkout step class, allowing
|
30
|
+
# us to specify a list of symbols in our wizard's .step macro
|
31
|
+
#
|
32
|
+
def self.for(identifier)
|
33
|
+
name = identifier.to_s.camelize
|
34
|
+
step_name = [name, 'CheckoutStep'].join
|
35
|
+
|
36
|
+
step = Stall::Utils.try_load_constant(step_name) ||
|
37
|
+
Stall::Utils.try_load_constant(['Stall', 'Checkout', step_name.demodulize].join('::'))
|
38
|
+
|
39
|
+
return step if step
|
40
|
+
|
41
|
+
raise StepNotFoundError,
|
42
|
+
"No checkout step was found for #{ identifier }. You can generate " +
|
43
|
+
"it with `rails g stall:checkout:step #{ identifier }`"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Stall
|
2
|
+
module Checkout
|
3
|
+
class Wizard
|
4
|
+
class StepUndefinedError < StandardError; end
|
5
|
+
|
6
|
+
attr_reader :cart
|
7
|
+
|
8
|
+
class_attribute :_steps
|
9
|
+
|
10
|
+
def self.steps(*identifiers)
|
11
|
+
if identifiers.length > 0
|
12
|
+
self._steps = identifiers
|
13
|
+
else
|
14
|
+
_steps
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(cart)
|
19
|
+
@cart = cart
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_step
|
23
|
+
if (step_name = current_step_name)
|
24
|
+
Stall::Checkout::Step.for(step_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_step_name
|
29
|
+
if step?(cart.state)
|
30
|
+
cart.state
|
31
|
+
else
|
32
|
+
raise StepUndefinedError,
|
33
|
+
"The current carte state #{ cart.state } does not exist " +
|
34
|
+
"for the current #{ self.class.name } wizard, which has the " +
|
35
|
+
"following steps : #{ steps.join(', ') }."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_step_name
|
40
|
+
step_name_for(step_index + 1) if step_index && step_index < steps_count
|
41
|
+
end
|
42
|
+
|
43
|
+
def complete?
|
44
|
+
step_index && step_index >= steps_count - 1
|
45
|
+
end
|
46
|
+
|
47
|
+
def steps_count
|
48
|
+
@steps_count ||= steps.length
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_current_step!
|
52
|
+
cart.state = next_step_name
|
53
|
+
cart.save!
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.route_key
|
57
|
+
name.gsub(/CheckoutWizard/, '').underscore.gsub('_', ' ').parameterize
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.from_route_key(key)
|
61
|
+
Stall::Utils.try_load_constant(
|
62
|
+
[key.underscore.camelize, 'CheckoutWizard'].join
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def steps
|
69
|
+
@steps ||= self.class.steps
|
70
|
+
end
|
71
|
+
|
72
|
+
def step_name_for(index)
|
73
|
+
steps[index]
|
74
|
+
end
|
75
|
+
|
76
|
+
def step_index
|
77
|
+
steps.index(cart.state)
|
78
|
+
end
|
79
|
+
|
80
|
+
def step?(state)
|
81
|
+
steps.include?(state)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|