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.
Files changed (137) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Rakefile +18 -0
  3. data/app/assets/images/shoppe/document.svg +1 -0
  4. data/app/assets/images/shoppe/logo.svg +47 -0
  5. data/app/assets/images/shoppe/shoppe.svg +25 -0
  6. data/app/assets/images/shoppe/square.svg +9 -0
  7. data/app/assets/images/shoppe/statuses/accepted.svg +14 -0
  8. data/app/assets/images/shoppe/statuses/paid.svg +16 -0
  9. data/app/assets/images/shoppe/statuses/received.svg +15 -0
  10. data/app/assets/images/shoppe/statuses/rejected.svg +14 -0
  11. data/app/assets/images/shoppe/statuses/shipped.svg +14 -0
  12. data/app/assets/javascripts/shoppe/application.coffee +11 -0
  13. data/app/assets/stylesheets/shoppe/application.scss +302 -0
  14. data/app/assets/stylesheets/shoppe/elements.scss +73 -0
  15. data/app/assets/stylesheets/shoppe/reset.scss +93 -0
  16. data/app/assets/stylesheets/shoppe/sub.scss +90 -0
  17. data/app/assets/stylesheets/shoppe/variables.scss +1 -0
  18. data/app/controllers/shoppe/application_controller.rb +39 -0
  19. data/app/controllers/shoppe/attachments_controller.rb +12 -0
  20. data/app/controllers/shoppe/dashboard_controller.rb +7 -0
  21. data/app/controllers/shoppe/delivery_service_prices_controller.rb +42 -0
  22. data/app/controllers/shoppe/delivery_services_controller.rb +45 -0
  23. data/app/controllers/shoppe/orders_controller.rb +36 -0
  24. data/app/controllers/shoppe/product_categories_controller.rb +45 -0
  25. data/app/controllers/shoppe/products_controller.rb +45 -0
  26. data/app/controllers/shoppe/sessions_controller.rb +31 -0
  27. data/app/controllers/shoppe/users_controller.rb +46 -0
  28. data/app/helpers/shoppe/application_helper.rb +10 -0
  29. data/app/helpers/shoppe/shoppe_helper.rb +28 -0
  30. data/app/mailers/shoppe/order_mailer.rb +25 -0
  31. data/app/mailers/shoppe/user_mailer.rb +10 -0
  32. data/app/models/shoppe/delivery_service.rb +22 -0
  33. data/app/models/shoppe/delivery_service_price.rb +19 -0
  34. data/app/models/shoppe/order.rb +325 -0
  35. data/app/models/shoppe/order_item.rb +113 -0
  36. data/app/models/shoppe/product.rb +56 -0
  37. data/app/models/shoppe/product_category.rb +22 -0
  38. data/app/models/shoppe/user.rb +41 -0
  39. data/app/views/layouts/shoppe/application.html.haml +35 -0
  40. data/app/views/layouts/shoppe/sub.html.haml +10 -0
  41. data/app/views/shoppe/delivery_service_prices/_form.html.haml +28 -0
  42. data/app/views/shoppe/delivery_service_prices/edit.html.haml +5 -0
  43. data/app/views/shoppe/delivery_service_prices/index.html.haml +22 -0
  44. data/app/views/shoppe/delivery_service_prices/new.html.haml +5 -0
  45. data/app/views/shoppe/delivery_services/_form.html.haml +35 -0
  46. data/app/views/shoppe/delivery_services/edit.html.haml +5 -0
  47. data/app/views/shoppe/delivery_services/index.html.haml +24 -0
  48. data/app/views/shoppe/delivery_services/new.html.haml +5 -0
  49. data/app/views/shoppe/order_mailer/accepted.text.erb +12 -0
  50. data/app/views/shoppe/order_mailer/received.text.erb +12 -0
  51. data/app/views/shoppe/order_mailer/rejected.text.erb +10 -0
  52. data/app/views/shoppe/order_mailer/shipped.text.erb +16 -0
  53. data/app/views/shoppe/orders/index.html.haml +60 -0
  54. data/app/views/shoppe/orders/show.html.haml +117 -0
  55. data/app/views/shoppe/product_categories/_form.html.haml +25 -0
  56. data/app/views/shoppe/product_categories/edit.html.haml +6 -0
  57. data/app/views/shoppe/product_categories/index.html.haml +15 -0
  58. data/app/views/shoppe/product_categories/new.html.haml +5 -0
  59. data/app/views/shoppe/products/_form.html.haml +71 -0
  60. data/app/views/shoppe/products/edit.html.haml +5 -0
  61. data/app/views/shoppe/products/index.html.haml +27 -0
  62. data/app/views/shoppe/products/new.html.haml +5 -0
  63. data/app/views/shoppe/sessions/new.html.haml +12 -0
  64. data/app/views/shoppe/sessions/reset.html.haml +11 -0
  65. data/app/views/shoppe/user_mailer/new_password.text.erb +9 -0
  66. data/app/views/shoppe/users/_form.html.haml +26 -0
  67. data/app/views/shoppe/users/edit.html.haml +5 -0
  68. data/app/views/shoppe/users/index.html.haml +17 -0
  69. data/app/views/shoppe/users/new.html.haml +5 -0
  70. data/config/routes.rb +24 -0
  71. data/config/shoppe.example.yml +6 -0
  72. data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +116 -0
  73. data/db/seeds.rb +85 -0
  74. data/db/seeds_data/poe400.jpg +0 -0
  75. data/db/seeds_data/snom-870-blk.jpg +0 -0
  76. data/db/seeds_data/snom-870-grey.jpg +0 -0
  77. data/db/seeds_data/snom-mm2.jpg +0 -0
  78. data/db/seeds_data/spa303.jpg +0 -0
  79. data/db/seeds_data/t18p.jpg +0 -0
  80. data/db/seeds_data/t20p.jpg +0 -0
  81. data/db/seeds_data/t22p.jpg +0 -0
  82. data/db/seeds_data/t26p.jpg +0 -0
  83. data/db/seeds_data/t41pn.jpg +0 -0
  84. data/db/seeds_data/t46gn.jpg +0 -0
  85. data/db/seeds_data/w52p.jpg +0 -0
  86. data/db/seeds_data/yhs32.jpg +0 -0
  87. data/lib/shoppe.rb +41 -0
  88. data/lib/shoppe/engine.rb +35 -0
  89. data/lib/shoppe/errors/inappropriate_delivery_service.rb +11 -0
  90. data/lib/shoppe/errors/insufficient_stock_to_fulfil.rb +19 -0
  91. data/lib/shoppe/errors/not_enough_stock.rb +19 -0
  92. data/lib/shoppe/errors/payment_declined.rb +15 -0
  93. data/lib/shoppe/setup_generator.rb +14 -0
  94. data/lib/shoppe/version.rb +3 -0
  95. data/lib/tasks/shoppe.rake +17 -0
  96. data/test/dummy/README.rdoc +28 -0
  97. data/test/dummy/Rakefile +6 -0
  98. data/test/dummy/app/assets/javascripts/application.js +13 -0
  99. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  100. data/test/dummy/app/controllers/application_controller.rb +5 -0
  101. data/test/dummy/app/helpers/application_helper.rb +2 -0
  102. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  103. data/test/dummy/bin/bundle +3 -0
  104. data/test/dummy/bin/rails +4 -0
  105. data/test/dummy/bin/rake +4 -0
  106. data/test/dummy/config.ru +4 -0
  107. data/test/dummy/config/application.rb +23 -0
  108. data/test/dummy/config/boot.rb +5 -0
  109. data/test/dummy/config/database.yml +25 -0
  110. data/test/dummy/config/environment.rb +5 -0
  111. data/test/dummy/config/environments/development.rb +29 -0
  112. data/test/dummy/config/environments/production.rb +80 -0
  113. data/test/dummy/config/environments/test.rb +36 -0
  114. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  115. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  116. data/test/dummy/config/initializers/inflections.rb +16 -0
  117. data/test/dummy/config/initializers/mime_types.rb +5 -0
  118. data/test/dummy/config/initializers/secret_token.rb +12 -0
  119. data/test/dummy/config/initializers/session_store.rb +3 -0
  120. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  121. data/test/dummy/config/locales/en.yml +23 -0
  122. data/test/dummy/config/routes.rb +3 -0
  123. data/test/dummy/config/shoppe.yml +7 -0
  124. data/test/dummy/db/development.sqlite3 +0 -0
  125. data/test/dummy/db/schema.rb +119 -0
  126. data/test/dummy/db/test.sqlite3 +0 -0
  127. data/test/dummy/log/development.log +893 -0
  128. data/test/dummy/log/test.log +6397 -0
  129. data/test/dummy/public/404.html +58 -0
  130. data/test/dummy/public/422.html +58 -0
  131. data/test/dummy/public/500.html +57 -0
  132. data/test/dummy/public/favicon.ico +0 -0
  133. data/test/models/shoppe/basket_test.rb +26 -0
  134. data/test/models/shoppe/user_test.rb +37 -0
  135. data/test/shoppe_test.rb +17 -0
  136. data/test/test_helper.rb +4 -0
  137. 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,10 @@
1
+ module Shoppe::ApplicationHelper
2
+
3
+ # Return all currencies with the currency provided by the Shoppe
4
+ # configuration file.
5
+ def number_to_currency(number, options = {})
6
+ options[:unit] ||= Shoppe.config[:currency_unit]
7
+ super
8
+ end
9
+
10
+ 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