shoppe 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +5 -6
  3. data/app/assets/javascripts/shoppe/application.coffee +19 -0
  4. data/app/assets/stylesheets/shoppe/application.scss +62 -20
  5. data/app/assets/stylesheets/shoppe/{chosen.css → chosen.scss} +18 -24
  6. data/app/assets/stylesheets/shoppe/dialog.scss +17 -2
  7. data/app/controllers/shoppe/orders_controller.rb +9 -7
  8. data/app/controllers/shoppe/payments_controller.rb +33 -0
  9. data/app/controllers/shoppe/settings_controller.rb +4 -0
  10. data/app/controllers/shoppe/tax_rates_controller.rb +1 -1
  11. data/app/controllers/shoppe/users_controller.rb +1 -1
  12. data/app/helpers/shoppe/application_helper.rb +0 -9
  13. data/app/models/shoppe/country.rb +17 -20
  14. data/app/models/shoppe/delivery_service.rb +16 -22
  15. data/app/models/shoppe/delivery_service_price.rb +10 -21
  16. data/app/models/shoppe/order/actions.rb +80 -0
  17. data/app/models/shoppe/order/billing.rb +99 -0
  18. data/app/models/shoppe/order/delivery.rb +196 -0
  19. data/app/models/shoppe/order/states.rb +69 -0
  20. data/app/models/shoppe/order.rb +29 -365
  21. data/app/models/shoppe/order_item.rb +52 -39
  22. data/app/models/shoppe/payment.rb +80 -0
  23. data/app/models/shoppe/product/product_attributes.rb +6 -4
  24. data/app/models/shoppe/product/variants.rb +20 -7
  25. data/app/models/shoppe/product.rb +37 -37
  26. data/app/models/shoppe/product_attribute.rb +13 -20
  27. data/app/models/shoppe/product_category.rb +6 -19
  28. data/app/models/shoppe/setting.rb +9 -0
  29. data/app/models/shoppe/stock_level_adjustment.rb +5 -18
  30. data/app/models/shoppe/tax_rate.rb +18 -21
  31. data/app/models/shoppe/user.rb +8 -15
  32. data/app/views/shoppe/delivery_service_prices/_form.html.haml +9 -3
  33. data/app/views/shoppe/delivery_service_prices/index.html.haml +6 -4
  34. data/app/views/shoppe/delivery_services/_form.html.haml +1 -1
  35. data/app/views/shoppe/orders/edit.html.haml +62 -0
  36. data/app/views/shoppe/orders/index.html.haml +2 -3
  37. data/app/views/shoppe/orders/show.html.haml +100 -63
  38. data/app/views/shoppe/payments/refund.html.haml +14 -0
  39. data/app/views/shoppe/product_categories/_form.html.haml +1 -1
  40. data/app/views/shoppe/products/_form.html.haml +8 -2
  41. data/app/views/shoppe/settings/edit.html.haml +1 -1
  42. data/app/views/shoppe/tax_rates/form.html.haml +5 -4
  43. data/app/views/shoppe/users/_form.html.haml +1 -1
  44. data/app/views/shoppe/variants/form.html.haml +2 -2
  45. data/config/routes.rb +3 -1
  46. data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +1 -1
  47. data/db/migrate/20131024201501_add_address_type_to_shoppe_tax_rates.rb +5 -0
  48. data/db/migrate/20131024204815_create_shoppe_payments.rb +32 -0
  49. data/db/schema.rb +218 -0
  50. data/db/seeds.rb +15 -15
  51. data/lib/shoppe/engine.rb +7 -2
  52. data/lib/shoppe/errors/refund_failed.rb +15 -0
  53. data/lib/shoppe/settings.rb +0 -2
  54. data/lib/shoppe/version.rb +1 -1
  55. data/lib/shoppe/view_helpers.rb +16 -0
  56. data/lib/shoppe.rb +23 -6
  57. data/test/app/db/schema.rb +21 -6
  58. data/test/app/log/development.log +12782 -0
  59. data/test/app/tmp/cache/assets/development/sass/edac894564dae62b78e653a08d1c41f10ade93f9/application.scssc +0 -0
  60. data/test/app/tmp/cache/assets/development/sass/edac894564dae62b78e653a08d1c41f10ade93f9/chosen.scssc +0 -0
  61. data/test/app/tmp/cache/assets/development/sprockets/01d6eb5fc12044a487be4b93584690f2 +0 -0
  62. data/test/app/tmp/cache/assets/development/sprockets/02d3923383f72b56dd7919e301d22d24 +0 -0
  63. data/test/app/tmp/cache/assets/development/sprockets/0a6bca3e510625f255083bd154cc470b +0 -0
  64. data/test/app/tmp/cache/assets/development/sprockets/2f80004fb2e2ce07283a83ac15cf920a +0 -0
  65. data/test/app/tmp/cache/assets/development/sprockets/322295abdd8625fcce4da08f9565cc63 +0 -0
  66. data/test/app/tmp/cache/assets/development/sprockets/4c8cb5cfd87990ebddbaa5b5fd594be5 +0 -0
  67. data/test/app/tmp/cache/assets/development/sprockets/53c0f5159a54836310b1a3f5357bc4c6 +0 -0
  68. data/test/app/tmp/cache/assets/development/sprockets/7938636d16e11b754d4dd046b89863c4 +0 -0
  69. data/test/app/tmp/cache/assets/development/sprockets/7a90d9251a7c5506f33a3c72a224e571 +0 -0
  70. data/test/app/tmp/cache/assets/development/sprockets/9da17bb4868a0b762f8884db45c76ffd +0 -0
  71. data/test/app/tmp/cache/assets/development/sprockets/a692ba7ed6cff183bb840c2622233c87 +0 -0
  72. data/test/app/tmp/cache/assets/development/sprockets/a9befe910d55141b8ba02d8198b8f966 +0 -0
  73. data/test/app/tmp/cache/assets/development/sprockets/accc4dc17ef18d0b510917a005340da5 +0 -0
  74. data/test/app/tmp/cache/assets/development/sprockets/b9ad7ea18b7e55c3626a15d1dae142ed +0 -0
  75. data/test/app/tmp/cache/assets/development/sprockets/c733f1a2fe9d05a3a634ff64a394f64b +0 -0
  76. data/test/app/tmp/cache/assets/development/sprockets/da76586dcb6d9a408b2cf33307790d66 +0 -0
  77. metadata +62 -63
  78. data/README.rdoc +0 -1
@@ -0,0 +1,80 @@
1
+ module Shoppe
2
+ class Order < ActiveRecord::Base
3
+
4
+ # These additional callbacks allow for applications to hook into other
5
+ # parts of the order lifecycle.
6
+ define_model_callbacks :confirmation, :acceptance, :rejection
7
+
8
+ # This method should be called by the base application when the user has completed their
9
+ # first round of entering details. This will mark the order as "confirming" which means
10
+ # the customer now just must confirm.
11
+ #
12
+ # @param params [Hash] a hash of order attributes
13
+ # @return [Boolean]
14
+ def proceed_to_confirm(params = {})
15
+ self.status = 'confirming'
16
+ if self.update(params)
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ # This method should be executed by the application when the order should be completed
24
+ # by the customer. It will raise exceptions if anything goes wrong or return true if
25
+ # the order has been confirmed successfully
26
+ #
27
+ # @return [Boolean]
28
+ def confirm!
29
+ no_stock_of = self.order_items.select(&:validate_stock_levels)
30
+ unless no_stock_of.empty?
31
+ raise Shoppe::Errors::InsufficientStockToFulfil, :order => self, :out_of_stock_items => no_stock_of
32
+ end
33
+
34
+ run_callbacks :confirmation do
35
+ # If we have successfully charged the card (i.e. no exception) we can go ahead and mark this
36
+ # order as 'received' which means it can be accepted by staff.
37
+ self.status = 'received'
38
+ self.received_at = Time.now
39
+ self.save!
40
+
41
+ self.order_items.each(&:confirm!)
42
+
43
+ # Send an email to the customer
44
+ Shoppe::OrderMailer.received(self).deliver
45
+ end
46
+
47
+ # We're all good.
48
+ true
49
+ end
50
+
51
+ # Mark order as accepted
52
+ #
53
+ # @param user [Shoppe::User] the user who carried out this action
54
+ def accept!(user)
55
+ run_callbacks :acceptance do
56
+ self.accepted_at = Time.now
57
+ self.accepted_by = user.id
58
+ self.status = 'accepted'
59
+ self.save!
60
+ self.order_items.each(&:accept!)
61
+ Shoppe::OrderMailer.accepted(self).deliver
62
+ end
63
+ end
64
+
65
+ # Mark order as rejected
66
+ #
67
+ # @param user [Shoppe::User] the user who carried out the action
68
+ def reject!(user)
69
+ run_callbacks :rejection do
70
+ self.rejected_at = Time.now
71
+ self.rejected_by = user.id
72
+ self.status = 'rejected'
73
+ self.save!
74
+ self.order_items.each(&:reject!)
75
+ Shoppe::OrderMailer.rejected(self).deliver
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,99 @@
1
+ module Shoppe
2
+ class Order < ActiveRecord::Base
3
+
4
+ # The country which this order should be billed to
5
+ #
6
+ # @return [Shoppe::Country]
7
+ belongs_to :billing_country, :class_name => 'Shoppe::Country', :foreign_key => 'billing_country_id'
8
+
9
+ # Payments which have been stored for the order
10
+ has_many :payments, :dependent => :destroy, :class_name => 'Shoppe::Payment'
11
+
12
+ # Validations
13
+ with_options :if => Proc.new { |o| !o.building? } do |order|
14
+ order.validates :first_name, :presence => true
15
+ order.validates :last_name, :presence => true
16
+ order.validates :billing_address1, :presence => true
17
+ order.validates :billing_address3, :presence => true
18
+ order.validates :billing_address4, :presence => true
19
+ order.validates :billing_postcode, :presence => true
20
+ order.validates :billing_country, :presence => true
21
+ end
22
+
23
+ # The name for billing purposes
24
+ #
25
+ # @return [String]
26
+ def billing_name
27
+ company.blank? ? full_name : "#{full_name} (#{company})"
28
+ end
29
+
30
+ # The total cost of the order
31
+ #
32
+ # @return [BigDecimal]
33
+ def total_cost
34
+ self.delivery_cost_price +
35
+ order_items.inject(BigDecimal(0)) { |t, i| t + i.total_cost }
36
+ end
37
+
38
+ # Return the price for the order
39
+ #
40
+ # @return [BigDecimal]
41
+ def profit
42
+ total_before_tax - total_cost
43
+ end
44
+
45
+ # The total price of the order before tax
46
+ #
47
+ # @return [BigDecimal]
48
+ def total_before_tax
49
+ self.delivery_price +
50
+ order_items.inject(BigDecimal(0)) { |t, i| t + i.sub_total }
51
+ end
52
+
53
+ # The total amount of tax due on this order
54
+ #
55
+ # @return [BigDecimal]
56
+ def tax
57
+ self.delivery_tax_amount +
58
+ order_items.inject(BigDecimal(0)) { |t, i| t + i.tax_amount }
59
+ end
60
+
61
+ # The total of the order including tax
62
+ #
63
+ # @return [BigDecimal]
64
+ def total
65
+ self.delivery_price +
66
+ self.delivery_tax_amount +
67
+ order_items.inject(BigDecimal(0)) { |t, i| t + i.total }
68
+ end
69
+
70
+ # The total amount due on the order
71
+ #
72
+ # @return [BigDecimal]
73
+ def balance
74
+ @balance ||= total - amount_paid
75
+ end
76
+
77
+ # Is there a payment still outstanding on this order?
78
+ #
79
+ # @return [Boolean]
80
+ def payment_outstanding?
81
+ balance > BigDecimal(0)
82
+ end
83
+
84
+ # Has this order been paid in full?
85
+ #
86
+ # @return [Boolean]
87
+ def paid_in_full?
88
+ !payment_outstanding?
89
+ end
90
+
91
+ # Is the order invoiced?
92
+ #
93
+ # @return [Boolean]
94
+ def invoiced?
95
+ !invoice_number.blank?
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,196 @@
1
+ module Shoppe
2
+ class Order < ActiveRecord::Base
3
+
4
+ # The associated delivery service
5
+ #
6
+ # @return [Shoppe::DeliveryService]
7
+ belongs_to :delivery_service, :class_name => 'Shoppe::DeliveryService'
8
+
9
+ # The country where this order is being delivered to (if one has been provided)
10
+ #
11
+ # @return [Shoppe::Country]
12
+ belongs_to :delivery_country, :class_name => 'Shoppe::Country', :foreign_key => 'delivery_country_id'
13
+
14
+ # The user who marked the order has shipped
15
+ #
16
+ # @return [Shoppe::User]
17
+ belongs_to :shipper, :class_name => 'Shoppe::User', :foreign_key => 'shipped_by'
18
+
19
+ # Set up a callback for use when an order is shipped
20
+ define_model_callbacks :ship
21
+
22
+ # Validations
23
+ with_options :if => :separate_delivery_address? do |order|
24
+ order.validates :delivery_name, :presence => true
25
+ order.validates :delivery_address1, :presence => true
26
+ order.validates :delivery_address3, :presence => true
27
+ order.validates :delivery_address4, :presence => true
28
+ order.validates :delivery_postcode, :presence => true
29
+ order.validates :delivery_country, :presence => true
30
+ end
31
+ validate do
32
+ unless available_delivery_services.include?(self.delivery_service)
33
+ errors.add :delivery_service_id, "is not suitable for this order"
34
+ end
35
+ end
36
+
37
+ before_confirmation do
38
+ # Ensure that before we confirm the order that the delivery service which has been selected
39
+ # is appropritae for the contents of the order.
40
+ unless self.valid_delivery_service?
41
+ raise Shoppe::Errors::InappropriateDeliveryService, :order => self
42
+ end
43
+
44
+ # Store the delivery prices with the order
45
+ if self.delivery_service
46
+ write_attribute :delivery_service_id, self.delivery_service.id
47
+ write_attribute :delivery_price, self.delivery_price
48
+ write_attribute :delivery_cost_price, self.delivery_cost_price
49
+ write_attribute :delivery_tax_amount, self.delivery_tax_amount
50
+ write_attribute :delivery_tax_rate, self.delivery_tax_rate
51
+ end
52
+ end
53
+
54
+ # If there isn't a seperate address needed, clear all the fields back to nil
55
+ before_validation do
56
+ unless separate_delivery_address?
57
+ self.delivery_name = nil
58
+ self.delivery_address1 = nil
59
+ self.delivery_address2 = nil
60
+ self.delivery_address3 = nil
61
+ self.delivery_address4 = nil
62
+ self.delivery_postcode = nil
63
+ self.delivery_country = nil
64
+ end
65
+ end
66
+
67
+ # Create some delivery_ methods which will mimic the billing methods if the order does
68
+ # not need a seperate address.
69
+ [:delivery_name, :delivery_address1, :delivery_address2, :delivery_address3, :delivery_address4, :delivery_postcode, :delivery_country].each do |f|
70
+ define_method(f) do
71
+ separate_delivery_address? ? super() : send(f.to_s.gsub('delivery_', 'billing_'))
72
+ end
73
+ end
74
+
75
+ # Has this order been shipped?
76
+ #
77
+ # @return [Boolean]
78
+ def shipped?
79
+ !!self.shipped_at?
80
+ end
81
+
82
+ # The total weight of the order
83
+ #
84
+ # @return [BigDecimal]
85
+ def total_weight
86
+ order_items.inject(BigDecimal(0)) { |t,i| t + i.weight}
87
+ end
88
+
89
+ # An array of all the delivery services which are suitable for this order in it's
90
+ # current state (based on its current weight)
91
+ #
92
+ # @return [Array] an array of Shoppe::DeliveryService objects
93
+ def available_delivery_services
94
+ @available_delivery_services ||= begin
95
+ delivery_service_prices.map(&:delivery_service).uniq
96
+ end
97
+ end
98
+
99
+ # An array of all the delivery service prices which can be applied to this order.
100
+ #
101
+ # @return [Array] an array of Shoppe:DeliveryServicePrice objects
102
+ def delivery_service_prices
103
+ @delivery_service_prices ||= begin
104
+ prices = Shoppe::DeliveryServicePrice.joins(:delivery_service).where(:shoppe_delivery_services => {:active => true}).order("`default` desc, price asc").for_weight(total_weight)
105
+ prices = prices.select { |p| p.countries.empty? || p.country?(self.delivery_country) }
106
+ prices
107
+ end
108
+ end
109
+
110
+ # The recommended delivery service for this order
111
+ #
112
+ # @return [Shoppe::DeliveryService]
113
+ def delivery_service
114
+ super || available_delivery_services.first
115
+ end
116
+
117
+ # Return the delivery price for this order in its current state
118
+ #
119
+ # @return [BigDecimal]
120
+ def delivery_service_price
121
+ @delivery_service_price ||= self.delivery_service && self.delivery_service.delivery_service_prices.for_weight(self.total_weight).first
122
+ end
123
+
124
+ # The price for delivering this order in its current state
125
+ #
126
+ # @return [BigDecimal]
127
+ def delivery_price
128
+ @delivery_price ||= read_attribute(:delivery_price) || delivery_service_price.try(:price) || 0.0
129
+ end
130
+
131
+ # The cost of delivering this order in its current state
132
+ #
133
+ # @return [BigDecimal]
134
+ def delivery_cost_price
135
+ @delivery_cost_price ||= read_attribute(:delivery_cost_price) || delivery_service_price.try(:cost_price) || 0.0
136
+ end
137
+
138
+ # The tax amount due for the delivery of this order in its current state
139
+ #
140
+ # @return [BigDecimal]
141
+ def delivery_tax_amount
142
+ @delivery_tax_amount ||= begin
143
+ read_attribute(:delivery_tax_amount) ||
144
+ delivery_price / BigDecimal(100) * delivery_tax_rate ||
145
+ 0.0
146
+ end
147
+ end
148
+
149
+ # The tax rate for the delivery of this order in its current state
150
+ #
151
+ # @return [BigDecimal]
152
+ def delivery_tax_rate
153
+ @delivery_tax_rate ||= begin
154
+ read_attribute(:delivery_tax_rate) ||
155
+ delivery_service_price.try(:tax_rate).try(:rate_for, self) ||
156
+ 0.0
157
+ end
158
+ end
159
+
160
+ # Is the currently assigned delivery service appropriate for this order?
161
+ #
162
+ # @return [Boolean]
163
+ def valid_delivery_service?
164
+ self.delivery_service && self.available_delivery_services.include?(self.delivery_service)
165
+ end
166
+
167
+ # Remove the associated delivery service if it's invalid
168
+ def remove_delivery_service_if_invalid
169
+ unless self.valid_delivery_service?
170
+ self.delivery_service = nil
171
+ self.save
172
+ end
173
+ end
174
+
175
+ # The URL which can be used to track the delivery of this order
176
+ #
177
+ # @return [String]
178
+ def courier_tracking_url
179
+ return nil if self.shipped_at.blank? || self.consignment_number.blank?
180
+ @courier_tracking_url ||= self.delivery_service.tracking_url_for(self)
181
+ end
182
+
183
+ # Mark this order as shipped
184
+ def ship!(user, consignment_number)
185
+ run_callbacks :ship do
186
+ self.shipped_at = Time.now
187
+ self.shipped_by = user.id
188
+ self.status = 'shipped'
189
+ self.consignment_number = consignment_number
190
+ self.save!
191
+ Shoppe::OrderMailer.shipped(self).deliver
192
+ end
193
+ end
194
+
195
+ end
196
+ end
@@ -0,0 +1,69 @@
1
+ module Shoppe
2
+ class Order < ActiveRecord::Base
3
+
4
+ # An array of all the available statuses for an order
5
+ STATUSES = ['building', 'confirming', 'received', 'accepted', 'rejected', 'shipped']
6
+
7
+ # The Shoppe::User who accepted the order
8
+ #
9
+ # @return [Shoppe::User]
10
+ belongs_to :accepter, :class_name => 'Shoppe::User', :foreign_key => 'accepted_by'
11
+
12
+ # The Shoppe::User who rejected the order
13
+ #
14
+ # @return [Shoppe::User]
15
+ belongs_to :rejecter, :class_name => 'Shoppe::User', :foreign_key => 'rejected_by'
16
+
17
+ # Validations
18
+ validates :status, :inclusion => {:in => STATUSES}
19
+
20
+ # Set the status to building if we don't have a status
21
+ before_validation { self.status = 'building' if self.status.blank? }
22
+
23
+ # All orders which have been received
24
+ scope :received, -> {where("received_at is not null")}
25
+
26
+ # All orders which are currently pending acceptance/rejection
27
+ scope :pending, -> { where(:status => 'received') }
28
+
29
+ # All ordered ordered by their ID desending
30
+ scope :ordered, -> { order('id desc')}
31
+
32
+ # Is this order still being built by the user?
33
+ #
34
+ # @return [Boolean]
35
+ def building?
36
+ self.status == 'building'
37
+ end
38
+
39
+ # Is this order in the user confirmation step?
40
+ #
41
+ # @return [Boolean]
42
+ def confirming?
43
+ self.status == 'confirming'
44
+ end
45
+
46
+ # Has this order been rejected?
47
+ #
48
+ # @return [Boolean]
49
+ def rejected?
50
+ !!self.rejected_at
51
+ end
52
+
53
+ # Has this order been accepted?
54
+ #
55
+ # @return [Boolean]
56
+ def accepted?
57
+ !!self.accepted_at
58
+ end
59
+
60
+ # Has the order been received?
61
+ #
62
+ # @return [Boolean]
63
+ def received?
64
+ !!self.received_at?
65
+ end
66
+
67
+
68
+ end
69
+ end