shoppe 0.0.5
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.
- data/MIT-LICENSE +20 -0
- data/Rakefile +18 -0
- data/app/assets/images/shoppe/document.svg +1 -0
- data/app/assets/images/shoppe/logo.svg +47 -0
- data/app/assets/images/shoppe/shoppe.svg +25 -0
- data/app/assets/images/shoppe/square.svg +9 -0
- data/app/assets/images/shoppe/statuses/accepted.svg +14 -0
- data/app/assets/images/shoppe/statuses/paid.svg +16 -0
- data/app/assets/images/shoppe/statuses/received.svg +15 -0
- data/app/assets/images/shoppe/statuses/rejected.svg +14 -0
- data/app/assets/images/shoppe/statuses/shipped.svg +14 -0
- data/app/assets/javascripts/shoppe/application.coffee +11 -0
- data/app/assets/stylesheets/shoppe/application.scss +302 -0
- data/app/assets/stylesheets/shoppe/elements.scss +73 -0
- data/app/assets/stylesheets/shoppe/reset.scss +93 -0
- data/app/assets/stylesheets/shoppe/sub.scss +90 -0
- data/app/assets/stylesheets/shoppe/variables.scss +1 -0
- data/app/controllers/shoppe/application_controller.rb +39 -0
- data/app/controllers/shoppe/attachments_controller.rb +12 -0
- data/app/controllers/shoppe/dashboard_controller.rb +7 -0
- data/app/controllers/shoppe/delivery_service_prices_controller.rb +42 -0
- data/app/controllers/shoppe/delivery_services_controller.rb +45 -0
- data/app/controllers/shoppe/orders_controller.rb +36 -0
- data/app/controllers/shoppe/product_categories_controller.rb +45 -0
- data/app/controllers/shoppe/products_controller.rb +45 -0
- data/app/controllers/shoppe/sessions_controller.rb +31 -0
- data/app/controllers/shoppe/users_controller.rb +46 -0
- data/app/helpers/shoppe/application_helper.rb +10 -0
- data/app/helpers/shoppe/shoppe_helper.rb +28 -0
- data/app/mailers/shoppe/order_mailer.rb +25 -0
- data/app/mailers/shoppe/user_mailer.rb +10 -0
- data/app/models/shoppe/delivery_service.rb +22 -0
- data/app/models/shoppe/delivery_service_price.rb +19 -0
- data/app/models/shoppe/order.rb +325 -0
- data/app/models/shoppe/order_item.rb +113 -0
- data/app/models/shoppe/product.rb +56 -0
- data/app/models/shoppe/product_category.rb +22 -0
- data/app/models/shoppe/user.rb +41 -0
- data/app/views/layouts/shoppe/application.html.haml +35 -0
- data/app/views/layouts/shoppe/sub.html.haml +10 -0
- data/app/views/shoppe/delivery_service_prices/_form.html.haml +28 -0
- data/app/views/shoppe/delivery_service_prices/edit.html.haml +5 -0
- data/app/views/shoppe/delivery_service_prices/index.html.haml +22 -0
- data/app/views/shoppe/delivery_service_prices/new.html.haml +5 -0
- data/app/views/shoppe/delivery_services/_form.html.haml +35 -0
- data/app/views/shoppe/delivery_services/edit.html.haml +5 -0
- data/app/views/shoppe/delivery_services/index.html.haml +24 -0
- data/app/views/shoppe/delivery_services/new.html.haml +5 -0
- data/app/views/shoppe/order_mailer/accepted.text.erb +12 -0
- data/app/views/shoppe/order_mailer/received.text.erb +12 -0
- data/app/views/shoppe/order_mailer/rejected.text.erb +10 -0
- data/app/views/shoppe/order_mailer/shipped.text.erb +16 -0
- data/app/views/shoppe/orders/index.html.haml +60 -0
- data/app/views/shoppe/orders/show.html.haml +117 -0
- data/app/views/shoppe/product_categories/_form.html.haml +25 -0
- data/app/views/shoppe/product_categories/edit.html.haml +6 -0
- data/app/views/shoppe/product_categories/index.html.haml +15 -0
- data/app/views/shoppe/product_categories/new.html.haml +5 -0
- data/app/views/shoppe/products/_form.html.haml +71 -0
- data/app/views/shoppe/products/edit.html.haml +5 -0
- data/app/views/shoppe/products/index.html.haml +27 -0
- data/app/views/shoppe/products/new.html.haml +5 -0
- data/app/views/shoppe/sessions/new.html.haml +12 -0
- data/app/views/shoppe/sessions/reset.html.haml +11 -0
- data/app/views/shoppe/user_mailer/new_password.text.erb +9 -0
- data/app/views/shoppe/users/_form.html.haml +26 -0
- data/app/views/shoppe/users/edit.html.haml +5 -0
- data/app/views/shoppe/users/index.html.haml +17 -0
- data/app/views/shoppe/users/new.html.haml +5 -0
- data/config/routes.rb +24 -0
- data/config/shoppe.example.yml +6 -0
- data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +116 -0
- data/db/seeds.rb +85 -0
- data/db/seeds_data/poe400.jpg +0 -0
- data/db/seeds_data/snom-870-blk.jpg +0 -0
- data/db/seeds_data/snom-870-grey.jpg +0 -0
- data/db/seeds_data/snom-mm2.jpg +0 -0
- data/db/seeds_data/spa303.jpg +0 -0
- data/db/seeds_data/t18p.jpg +0 -0
- data/db/seeds_data/t20p.jpg +0 -0
- data/db/seeds_data/t22p.jpg +0 -0
- data/db/seeds_data/t26p.jpg +0 -0
- data/db/seeds_data/t41pn.jpg +0 -0
- data/db/seeds_data/t46gn.jpg +0 -0
- data/db/seeds_data/w52p.jpg +0 -0
- data/db/seeds_data/yhs32.jpg +0 -0
- data/lib/shoppe.rb +41 -0
- data/lib/shoppe/engine.rb +35 -0
- data/lib/shoppe/errors/inappropriate_delivery_service.rb +11 -0
- data/lib/shoppe/errors/insufficient_stock_to_fulfil.rb +19 -0
- data/lib/shoppe/errors/not_enough_stock.rb +19 -0
- data/lib/shoppe/errors/payment_declined.rb +15 -0
- data/lib/shoppe/setup_generator.rb +14 -0
- data/lib/shoppe/version.rb +3 -0
- data/lib/tasks/shoppe.rake +17 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/config/shoppe.yml +7 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/schema.rb +119 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +893 -0
- data/test/dummy/log/test.log +6397 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/models/shoppe/basket_test.rb +26 -0
- data/test/models/shoppe/user_test.rb +37 -0
- data/test/shoppe_test.rb +17 -0
- data/test/test_helper.rb +4 -0
- metadata +453 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Shoppe::ProductCategoriesController < Shoppe::ApplicationController
|
|
2
|
+
|
|
3
|
+
before_filter { @active_nav = :product_categories }
|
|
4
|
+
before_filter { params[:id] && @product_category = Shoppe::ProductCategory.find(params[:id]) }
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@product_categories = Shoppe::ProductCategory.ordered.all
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@product_category = Shoppe::ProductCategory.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@product_category = Shoppe::ProductCategory.new(safe_params)
|
|
16
|
+
if @product_category.save
|
|
17
|
+
redirect_to :product_categories, :flash => {:notice => "Category has been created successfully"}
|
|
18
|
+
else
|
|
19
|
+
render :action => "new"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def edit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @product_category.update(safe_params)
|
|
28
|
+
redirect_to [:edit, @product_category], :flash => {:notice => "Category has been updated successfully"}
|
|
29
|
+
else
|
|
30
|
+
render :action => "edit"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
@product_category.destroy
|
|
36
|
+
redirect_to :product_categories, :flash => {:notice => "Category has been removed successfully"}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def safe_params
|
|
42
|
+
params[:product_category].permit(:name, :permalink, :description, :image_file)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Shoppe::ProductsController < Shoppe::ApplicationController
|
|
2
|
+
|
|
3
|
+
before_filter { @active_nav = :products }
|
|
4
|
+
before_filter { params[:id] && @product = Shoppe::Product.find(params[:id]) }
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@products = Shoppe::Product.includes(:default_image, :product_category).order(:title).group_by(&:product_category).sort_by { |cat,pro| cat.name }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@product = Shoppe::Product.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@product = Shoppe::Product.new(safe_params)
|
|
16
|
+
if @product.save
|
|
17
|
+
redirect_to :products, :flash => {:notice => "Product has been created successfully"}
|
|
18
|
+
else
|
|
19
|
+
render :action => "new"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def edit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @product.update(safe_params)
|
|
28
|
+
redirect_to [:edit, @product], :flash => {:notice => "Product has been updated successfully"}
|
|
29
|
+
else
|
|
30
|
+
render :action => "edit"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
@product.destroy
|
|
36
|
+
redirect_to :products, :flash => {:notice => "Product has been removed successfully"}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def safe_params
|
|
42
|
+
params[:product].permit(:product_category_id, :title, :sku, :permalink, :description, :short_description, :weight, :price, :tax_rate, :stock, :default_image_file, :data_sheet_file, :active, :featured, :in_the_box)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Shoppe::SessionsController < Shoppe::ApplicationController
|
|
2
|
+
layout 'shoppe/sub'
|
|
3
|
+
skip_before_filter :login_required, :only => [:new, :create, :reset]
|
|
4
|
+
|
|
5
|
+
def create
|
|
6
|
+
if user = Shoppe::User.authenticate(params[:email_address], params[:password])
|
|
7
|
+
session[:shoppe_user_id] = user.id
|
|
8
|
+
redirect_to :orders
|
|
9
|
+
else
|
|
10
|
+
flash.now[:alert] = "The email address and/or password you have entered is invalid. Please check and try again."
|
|
11
|
+
render :action => "new"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def destroy
|
|
16
|
+
session[:shoppe_user_id] = nil
|
|
17
|
+
redirect_to :login
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset
|
|
21
|
+
|
|
22
|
+
if request.post?
|
|
23
|
+
if user = Shoppe::User.find_by_email_address(params[:email_address])
|
|
24
|
+
user.reset_password!
|
|
25
|
+
redirect_to login_path(:email_address => params[:email_address]), :notice => "An e-mail has been sent to #{user.email_address} with a new password"
|
|
26
|
+
else
|
|
27
|
+
flash.now[:alert] = "No user was found matching the e-mail address"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class Shoppe::UsersController < Shoppe::ApplicationController
|
|
2
|
+
|
|
3
|
+
before_filter { @active_nav = :users }
|
|
4
|
+
before_filter { params[:id] && @user = Shoppe::User.find(params[:id]) }
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@users = Shoppe::User.all
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def new
|
|
11
|
+
@user = Shoppe::User.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
@user = Shoppe::User.new(safe_params)
|
|
16
|
+
if @user.save
|
|
17
|
+
redirect_to :users, :flash => {:notice => "User has been created successfully"}
|
|
18
|
+
else
|
|
19
|
+
render :action => "new"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def edit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if @user.update(safe_params)
|
|
28
|
+
redirect_to [:edit, @user], :flash => {:notice => "User has been updated successfully"}
|
|
29
|
+
else
|
|
30
|
+
render :action => "edit"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy
|
|
35
|
+
raise Shoppe::Error, "You cannot remove yourself" if @user == current_user
|
|
36
|
+
@user.destroy
|
|
37
|
+
redirect_to :users, :flash => {:notice => "User has been removed successfully"}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def safe_params
|
|
43
|
+
params[:user].permit(:first_name, :last_name, :email_address, :password, :password_confirmation)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Shoppe::ShoppeHelper
|
|
2
|
+
|
|
3
|
+
def status_tag(status)
|
|
4
|
+
content_tag :span, status, :class => "status-tag #{status}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def attachment_preview(attachment)
|
|
8
|
+
if attachment
|
|
9
|
+
String.new.tap do |s|
|
|
10
|
+
if attachment.image?
|
|
11
|
+
style = "style='background-image:url(#{attachment.path})'"
|
|
12
|
+
else
|
|
13
|
+
style = ''
|
|
14
|
+
end
|
|
15
|
+
s << "<div class='attachmentPreview #{attachment.image? ? 'image' : 'doc'}'>"
|
|
16
|
+
s << "<div class='imgContainer'><div class='img' #{style}></div></div>"
|
|
17
|
+
s << "<div class='desc'>"
|
|
18
|
+
s << "<span class='filename'><a href='#{attachment_path(attachment)}'>#{attachment.file_name}</a></span>"
|
|
19
|
+
s << "<span class='delete'>"
|
|
20
|
+
s << link_to("Delete this file?", attachment_path(attachment), :method => :delete, :data => {:confirm => "Are you sure you wish to remove this attachment?"})
|
|
21
|
+
s << "</span>"
|
|
22
|
+
s << "</div>"
|
|
23
|
+
s << "</div>"
|
|
24
|
+
end.html_safe
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Shoppe::OrderMailer < ActionMailer::Base
|
|
2
|
+
|
|
3
|
+
default :from => "#{Shoppe.config[:store_name]} <#{Shoppe.config[:email_address]}>"
|
|
4
|
+
|
|
5
|
+
def received(order)
|
|
6
|
+
@order = order
|
|
7
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.subject', :default => "Order Confirmation")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def accepted(order)
|
|
11
|
+
@order = order
|
|
12
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.accepted', :default => "Order Accepted")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def rejected(order)
|
|
16
|
+
@order = order
|
|
17
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.rejected', :default => "Order Rejected")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def shipped(order)
|
|
21
|
+
@order = order
|
|
22
|
+
mail :to => order.email_address, :subject => I18n.t('shoppe.order_mailer.received.shipped', :default => "Order Shipped")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class UserMailer < ActionMailer::Base
|
|
3
|
+
default :from => "#{Shoppe.config[:store_name]} <#{Shoppe.config[:email_address]}>"
|
|
4
|
+
|
|
5
|
+
def new_password(user)
|
|
6
|
+
@user = user
|
|
7
|
+
mail :to => user.email_address, :subject => "Your new Shoppe password"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Shoppe::DeliveryService < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
# Set the table name
|
|
4
|
+
self.table_name = 'shoppe_delivery_services'
|
|
5
|
+
|
|
6
|
+
# Validations
|
|
7
|
+
validates :name, :presence => true
|
|
8
|
+
validates :courier, :presence => true
|
|
9
|
+
|
|
10
|
+
# Relationships
|
|
11
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Order'
|
|
12
|
+
has_many :delivery_service_prices, :dependent => :destroy, :class_name => 'Shoppe::DeliveryServicePrice'
|
|
13
|
+
|
|
14
|
+
# Scopes
|
|
15
|
+
scope :active, -> { where(:active => true)}
|
|
16
|
+
|
|
17
|
+
# Return the tracking URL for the given consignment number
|
|
18
|
+
def tracking_url_for(consignment_number)
|
|
19
|
+
tracking_url.gsub("{{consignment_number}}", consignment_number)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class Shoppe::DeliveryServicePrice < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
# Set the table name
|
|
4
|
+
self.table_name = 'shoppe_delivery_service_prices'
|
|
5
|
+
|
|
6
|
+
# Relationships
|
|
7
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
|
8
|
+
|
|
9
|
+
# Validations
|
|
10
|
+
validates :price, :numericality => true
|
|
11
|
+
validates :tax_rate, :numericality => true
|
|
12
|
+
validates :min_weight, :numericality => true
|
|
13
|
+
validates :max_weight, :numericality => true
|
|
14
|
+
|
|
15
|
+
# Scopes
|
|
16
|
+
scope :ordered, -> { order('price asc')}
|
|
17
|
+
scope :for_weight, -> weight { where("min_weight <= ? AND max_weight >= ?", weight, weight) }
|
|
18
|
+
|
|
19
|
+
end
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
class Shoppe::Order < ActiveRecord::Base
|
|
2
|
+
|
|
3
|
+
# Set the table name
|
|
4
|
+
self.table_name = 'shoppe_orders'
|
|
5
|
+
|
|
6
|
+
# An array of all the available statuses for an order
|
|
7
|
+
STATUSES = ['building', 'confirming', 'received', 'accepted', 'rejected', 'shipped']
|
|
8
|
+
|
|
9
|
+
# Order's implement a key value store for storing arbitary properties which
|
|
10
|
+
# may be useful (for example payment configuraiton)
|
|
11
|
+
key_value_store :properties
|
|
12
|
+
|
|
13
|
+
# These additional callbacks allow for applications to hook into other
|
|
14
|
+
# parts of the order lifecycle.
|
|
15
|
+
define_model_callbacks :confirmation, :payment, :acceptance, :rejection, :ship
|
|
16
|
+
|
|
17
|
+
# Relationships
|
|
18
|
+
belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
|
|
19
|
+
belongs_to :accepter, :class_name => 'Shoppe::User', :foreign_key => 'accepted_by'
|
|
20
|
+
belongs_to :rejecter, :class_name => 'Shoppe::User', :foreign_key => 'rejected_by'
|
|
21
|
+
belongs_to :shipper, :class_name => 'Shoppe::User', :foreign_key => 'shipped_by'
|
|
22
|
+
has_many :order_items, :dependent => :destroy, :class_name => 'Shoppe::OrderItem'
|
|
23
|
+
has_many :products, :through => :order_items, :class_name => 'Shoppe::Product'
|
|
24
|
+
|
|
25
|
+
# Validations
|
|
26
|
+
validates :token, :presence => true
|
|
27
|
+
validates :status, :inclusion => {:in => STATUSES}
|
|
28
|
+
with_options :if => Proc.new { |o| !o.building? } do |order|
|
|
29
|
+
order.validates :first_name, :presence => true
|
|
30
|
+
order.validates :last_name, :presence => true
|
|
31
|
+
order.validates :address1, :presence => true
|
|
32
|
+
order.validates :address2, :presence => true
|
|
33
|
+
order.validates :postcode, :presence => true
|
|
34
|
+
order.validates :country, :presence => true
|
|
35
|
+
order.validates :email_address, :format => {:with => /\A\b[A-Z0-9\.\_\%\-\+]+@(?:[A-Z0-9\-]+\.)+[A-Z]{2,6}\b\z/i}
|
|
36
|
+
order.validates :phone_number, :format => {:with => /\A[\d\ ]{7,}\z/}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Scopes
|
|
40
|
+
scope :received, -> {where("received_at is not null")}
|
|
41
|
+
scope :pending, -> { where(:status => 'received') }
|
|
42
|
+
scope :ordered, -> { order('id desc')}
|
|
43
|
+
|
|
44
|
+
# Set some defaults
|
|
45
|
+
before_validation do
|
|
46
|
+
self.status = 'building' if self.status.blank?
|
|
47
|
+
self.token = SecureRandom.uuid if self.token.blank?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Is this order still being built by the user?
|
|
51
|
+
def building?
|
|
52
|
+
self.status == 'building'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Is this order in the user confirmation step?
|
|
56
|
+
def confirming?
|
|
57
|
+
self.status == 'confirming'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Has this order been rejected?
|
|
61
|
+
def rejected?
|
|
62
|
+
!!self.rejected_at
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Has this order been accepted?
|
|
66
|
+
def accepted?
|
|
67
|
+
!!self.accepted_at
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Has this order been shipped?
|
|
71
|
+
def shipped?
|
|
72
|
+
!!self.shipped_at?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Has the order been received?
|
|
76
|
+
def received?
|
|
77
|
+
!!self.received_at?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The order number
|
|
81
|
+
def number
|
|
82
|
+
id.to_s.rjust(6, '0')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The length of time the customer spent building the order before submitting it to us.
|
|
86
|
+
# The time from first item in basket to received.
|
|
87
|
+
def build_time
|
|
88
|
+
return nil if self.received_at.blank?
|
|
89
|
+
self.created_at - self.received_at
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The name of the customer
|
|
93
|
+
def customer_name
|
|
94
|
+
company.blank? ? "#{first_name} #{last_name}" : "#{company} (#{first_name} #{last_name})"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Is this order empty? (i.e. doesn't have any items associated with it)
|
|
98
|
+
def empty?
|
|
99
|
+
order_items.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Does this order have items?
|
|
103
|
+
def has_items?
|
|
104
|
+
total_items > 0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Return the number of items in the order?
|
|
108
|
+
def total_items
|
|
109
|
+
@total_items ||= order_items.inject(0) { |t,i| t + i.quantity }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The total price of the order before tax
|
|
113
|
+
def total_before_tax
|
|
114
|
+
self.delivery_price +
|
|
115
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.sub_total }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The total amount of tax due on this order
|
|
119
|
+
def tax
|
|
120
|
+
self.delivery_tax_amount +
|
|
121
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.tax_amount }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The total of the order including tax
|
|
125
|
+
def total
|
|
126
|
+
self.delivery_price +
|
|
127
|
+
self.delivery_tax_amount +
|
|
128
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The total of the order including tax in pence
|
|
132
|
+
def total_in_pence
|
|
133
|
+
(total * BigDecimal(100)).to_i
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The total weight of the order
|
|
137
|
+
def total_weight
|
|
138
|
+
order_items.inject(BigDecimal(0)) { |t,i| t + i.weight}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# An array of all the delivery service prices which can be applied to this order.
|
|
142
|
+
def delivery_service_prices
|
|
143
|
+
@delivery_service_prices ||= begin
|
|
144
|
+
Shoppe::DeliveryServicePrice.joins(:delivery_service).where(:shoppe_delivery_services => {:active => true}).order("`default` desc, price asc").for_weight(total_weight)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# An array of all the delivery services which are suitable for this order in it's
|
|
149
|
+
# current state (based on its current weight)
|
|
150
|
+
def available_delivery_services
|
|
151
|
+
@available_delivery_services ||= begin
|
|
152
|
+
delivery_service_prices.map(&:delivery_service).uniq
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The recommended delivery service for this order
|
|
157
|
+
def delivery_service
|
|
158
|
+
super || available_delivery_services.first
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Return the delivery price for this order in its current state
|
|
162
|
+
def delivery_service_price
|
|
163
|
+
@delivery_service_price ||= self.delivery_service && self.delivery_service.delivery_service_prices.for_weight(self.total_weight).first
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The cost of delivering this order in its current state
|
|
167
|
+
def delivery_price
|
|
168
|
+
@delivery_price ||= read_attribute(:delivery_price) || delivery_service_price.try(:price) || 0.0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The tax amount due for the delivery of this order in its current state
|
|
172
|
+
def delivery_tax_amount
|
|
173
|
+
@delivery_tax_amount ||= begin
|
|
174
|
+
read_attribute(:delivery_tax_amount) ||
|
|
175
|
+
(delivery_service_price ? delivery_price / BigDecimal(100) * delivery_service_price.tax_rate : 0.0) ||
|
|
176
|
+
0.0
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The tax rate for the delivery of this order in its current state
|
|
181
|
+
def delivery_tax_rate
|
|
182
|
+
@delivery_tax_rate ||= begin
|
|
183
|
+
read_attribute(:delivery_tax_rate) ||
|
|
184
|
+
delivery_service_price.try(:tax_rate) ||
|
|
185
|
+
0.0
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Is the currently assigned delivery service appropriate for this order?
|
|
190
|
+
def valid_delivery_service?
|
|
191
|
+
self.delivery_service && self.available_delivery_services.include?(self.delivery_service)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Remove the associated delivery service if it's invalid
|
|
195
|
+
def remove_delivery_service_if_invalid
|
|
196
|
+
unless self.valid_delivery_service?
|
|
197
|
+
self.delivery_service = nil
|
|
198
|
+
self.save
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# The URL which can be used to track the delivery of this order
|
|
203
|
+
def courier_tracking_url
|
|
204
|
+
return nil if self.shipped_at.blank? || self.consignment_number.blank?
|
|
205
|
+
@courier_tracking_url ||= self.delivery_service.tracking_url_for(self.consignment_number)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Has this order been fully paid for?
|
|
209
|
+
def paid?
|
|
210
|
+
!paid_at.blank?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# This method is called by the customer when they submit their details in the first step of
|
|
214
|
+
# the checkout process. It will update the status to 'confirmed' as well as updating their
|
|
215
|
+
# details. Any issues with validation will cause false to be returned otherwise true. Any
|
|
216
|
+
# more serious issues will be raised as exceptions.
|
|
217
|
+
def proceed_to_confirm(params = {})
|
|
218
|
+
self.status = 'confirming'
|
|
219
|
+
if self.update(params)
|
|
220
|
+
true
|
|
221
|
+
else
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# This method will confirm the order If there are any issues with the order an exception
|
|
227
|
+
# should be raised.
|
|
228
|
+
def confirm!
|
|
229
|
+
|
|
230
|
+
# Ensure that we have the stock to fulfil this order at the current time. We may have had it when
|
|
231
|
+
# it was placed int he basket and if we don't now, we should let the user know so they can
|
|
232
|
+
# rethink.
|
|
233
|
+
no_stock_of = self.order_items.select(&:validate_stock_levels)
|
|
234
|
+
unless no_stock_of.empty?
|
|
235
|
+
raise Shoppe::Errors::InsufficientStockToFulfil, :order => self, :out_of_stock_items => no_stock_of
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Ensure that before we confirm the order that the delivery service which has been selected
|
|
239
|
+
# is appropritae for the contents of the order.
|
|
240
|
+
unless self.valid_delivery_service?
|
|
241
|
+
raise Shoppe::Errors::InappropriateDeliveryService, :order => self
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Store the delivery prices with the order
|
|
245
|
+
if self.delivery_service
|
|
246
|
+
write_attribute :delivery_service_id, self.delivery_service.id
|
|
247
|
+
write_attribute :delivery_price, self.delivery_price
|
|
248
|
+
write_attribute :delivery_tax_amount, self.delivery_tax_amount
|
|
249
|
+
write_attribute :delivery_tax_rate, self.delivery_tax_rate
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
run_callbacks :confirmation do
|
|
253
|
+
# If we have successfully charged the card (i.e. no exception) we can go ahead and mark this
|
|
254
|
+
# order as 'received' which means it can be accepted by staff.
|
|
255
|
+
self.status = 'received'
|
|
256
|
+
self.received_at = Time.now
|
|
257
|
+
self.save!
|
|
258
|
+
|
|
259
|
+
self.order_items.each(&:confirm!)
|
|
260
|
+
|
|
261
|
+
# Send an email to the customer
|
|
262
|
+
Shoppe::OrderMailer.received(self).deliver
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# We're all good.
|
|
266
|
+
true
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# This method will mark an order as paid.
|
|
270
|
+
def pay!(reference, method)
|
|
271
|
+
run_callbacks :payment do
|
|
272
|
+
self.paid_at = Time.now.utc
|
|
273
|
+
self.payment_reference = reference
|
|
274
|
+
self.payment_method = method
|
|
275
|
+
self.save!
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# This method will accept the this order. It is called by a user (which is the only
|
|
280
|
+
# parameter).
|
|
281
|
+
def accept!(user)
|
|
282
|
+
run_callbacks :acceptance do
|
|
283
|
+
self.accepted_at = Time.now
|
|
284
|
+
self.accepted_by = user.id
|
|
285
|
+
self.status = 'accepted'
|
|
286
|
+
self.save!
|
|
287
|
+
Shoppe::OrderMailer.accepted(self).deliver
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# This method will reject the order. It is called by a user (which is the only parameter).
|
|
292
|
+
def reject!(user)
|
|
293
|
+
run_callbacks :rejection do
|
|
294
|
+
self.rejected_at = Time.now
|
|
295
|
+
self.rejected_by = user.id
|
|
296
|
+
self.status = 'rejected'
|
|
297
|
+
self.save!
|
|
298
|
+
Shoppe::OrderMailer.rejected(self).deliver
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# This method will mark an order as shipped and store the given consignment number with the
|
|
303
|
+
# order for use later in tracking.
|
|
304
|
+
def ship!(user, consignment_number)
|
|
305
|
+
run_callbacks :ship do
|
|
306
|
+
self.shipped_at = Time.now
|
|
307
|
+
self.shipped_by = user.id
|
|
308
|
+
self.status = 'shipped'
|
|
309
|
+
self.consignment_number = consignment_number
|
|
310
|
+
self.save!
|
|
311
|
+
Shoppe::OrderMailer.shipped(self).deliver
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Specify which attributes can be searched
|
|
316
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
317
|
+
["id", "postcode", "address1", "address2", "address3", "address4", "first_name", "last_name", "company", "email_address", "phone_number", "consignment_number", "status", "received_at"] + _ransackers.keys
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Specify which associations can be searched
|
|
321
|
+
def self.ransackable_associations(auth_object = nil)
|
|
322
|
+
['products']
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
end
|