piggybak 0.5.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.md +32 -25
  2. data/VERSION +1 -1
  3. data/app/assets/javascripts/piggybak.js +11 -1
  4. data/app/assets/javascripts/piggybak.states.js +6 -5
  5. data/app/controllers/piggybak/orders_controller.rb +14 -22
  6. data/app/models/piggybak/address.rb +4 -1
  7. data/app/models/piggybak/adjustment.rb +1 -25
  8. data/app/models/piggybak/cart.rb +18 -17
  9. data/app/models/piggybak/line_item.rb +106 -19
  10. data/app/models/piggybak/order.rb +94 -108
  11. data/app/models/piggybak/payment.rb +25 -27
  12. data/app/models/piggybak/payment_method.rb +3 -1
  13. data/app/models/piggybak/payment_method_value.rb +3 -1
  14. data/app/models/piggybak/{variant.rb → sellable.rb} +6 -4
  15. data/app/models/piggybak/shipment.rb +4 -10
  16. data/app/models/piggybak/shipping_calculator/flat_rate.rb +4 -0
  17. data/app/models/piggybak/shipping_calculator/free.rb +4 -0
  18. data/app/models/piggybak/shipping_calculator/range.rb +4 -0
  19. data/app/models/piggybak/shipping_method.rb +1 -1
  20. data/app/models/piggybak/tax_method.rb +1 -1
  21. data/app/views/piggybak/cart/_form.html.erb +7 -7
  22. data/app/views/piggybak/cart/_items.html.erb +14 -5
  23. data/app/views/piggybak/notifier/order_notification.text.erb +10 -2
  24. data/app/views/piggybak/orders/_details.html.erb +14 -5
  25. data/app/views/piggybak/orders/_google_analytics.html.erb +3 -3
  26. data/app/views/piggybak/orders/download.text.erb +6 -6
  27. data/app/views/piggybak/orders/submit.html.erb +55 -49
  28. data/app/views/rails_admin/main/_location_select.html.haml +3 -3
  29. data/app/views/rails_admin/main/_order_details.html.erb +12 -24
  30. data/app/views/rails_admin/main/_polymorphic_nested.html.haml +29 -0
  31. data/bin/piggybak +12 -0
  32. data/config/routes.rb +0 -2
  33. data/db/migrate/20121008160425_rename_variants_to_sellables.rb +11 -0
  34. data/db/migrate/20121008175144_line_item_rearchitecture.rb +96 -0
  35. data/lib/acts_as_sellable.rb +21 -0
  36. data/lib/formatted_changes.rb +2 -2
  37. data/lib/piggybak.rb +80 -126
  38. data/lib/piggybak/cli.rb +81 -0
  39. data/lib/piggybak/config.rb +21 -0
  40. data/piggybak.gemspec +10 -7
  41. data/spec/dummy_app/app/models/image.rb +1 -1
  42. data/spec/factories.rb +1 -1
  43. metadata +52 -49
  44. data/app/controllers/piggybak/payments_controller.rb +0 -14
  45. data/app/views/rails_admin/main/_order_notes.html.erb +0 -1
  46. data/app/views/rails_admin/main/_payment_refund.html.haml +0 -6
  47. data/lib/acts_as_variant.rb +0 -15
data/README.md CHANGED
@@ -11,46 +11,47 @@ Modular / mountable ecommerce gem. Features:
11
11
 
12
12
  * Fully defined backend RailsAdmin interface for adding orders on the backend
13
13
 
14
+
15
+ Announcements
16
+ ========
17
+
18
+ * Variants were recently changed to sellables, to provide the opportunity for advanced variant support via an extension.
19
+
20
+ * Significant recent rearchitecture has been applied to the order line items. Stay tuned for the documentation.
21
+
22
+ * Review the new installation process below.
23
+
24
+
14
25
  Installation
15
26
  ========
16
27
 
17
- * First, add to Gemfile (from RubyGems, with version specified, or source) with *one* of the following options:
28
+ * First create a new rails project:
29
+ rails new webstore
30
+
31
+ * Config your database.yml and create the databases
32
+
33
+ * Add to Gemfile:
18
34
 
19
35
  gem "piggybak"
20
- gem "piggybak", '0.4.19'
21
- gem "piggybak", :git => "git://github.com/stephskardal/piggybak.git"
22
36
 
23
- * Next, run rake task to copy migrations:
37
+ * Next, run bundle install:
24
38
 
25
- rake piggybak_engine:install:migrations
39
+ bundle install
26
40
 
27
- * Next, run rake task to run migrations:
41
+ * Next, run the piggybak install command:
28
42
 
29
- rake db:migrate
43
+ piggybak install
30
44
 
31
- * Next, mount in your application by adding:
45
+ (NOTE: If you run into an error saying that piggybak gem is missing, use bundle exec piggybak install)
32
46
 
33
- mount Piggybak::Engine => '/checkout', :as => 'piggybak'" to config/routes
34
-
35
- * Add acts_as_variant to any model that will become a sellable item.
47
+ * Piggybak is now installed and ready to be added to whatever model class will be sold.
36
48
 
37
49
  class Product < ActiveRecord::Base
38
- acts_as_variant
50
+ acts_as_sellable
39
51
  end
40
52
 
41
- * You must include jquery_ujs in your application.js file in to get the remove item from cart functionality to work.
42
-
43
- //= require jquery_ujs
44
-
45
- * You must add the following to your application layout:
46
-
47
- <% if "#{params[:controller]}##{params[:action]}" == "piggybak/orders#submit" -%>
48
- <%= javascript_include_tag "piggybak-application" %>
49
- <% end -%>
53
+ * Piggybak checkout is located at /checkout
50
54
 
51
- * And you must add this to your production configuration, in order for this asset to be precompiled (and in some cases, ensure that it is served via SSL):
52
-
53
- config.assets.precompile += %w( piggybak-application.js )
54
55
 
55
56
  More Details
56
57
  ========
@@ -62,7 +63,13 @@ Visit the project website [here][project-website] to see more documentation and
62
63
  TODO
63
64
  ========
64
65
 
65
- On order notes functionality, changes in addresses are not recorded. This functionality is broken and needs attention.
66
+ * Ensure that changes in nested addresses are recorded on order notes.
67
+
68
+ * Add admin side validation to limit 1 payment at a time
69
+
70
+ * Add/check validation to ensure sufficient inventory
71
+
72
+ * Add copy from billing above shipping address section button
66
73
 
67
74
  Copyright
68
75
  ========
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.5
1
+ 0.6.0
@@ -4,7 +4,8 @@ var page_load = 1;
4
4
  var shipping_field;
5
5
 
6
6
  $(function() {
7
- shipping_field = $('#piggybak_order_shipments_attributes_0_shipping_method_id');
7
+ piggybak.prevent_double_click();
8
+ shipping_field = $('#piggybak_order_line_items_attributes_0_shipment_attributes_shipping_method_id');
8
9
  shipping_els = $('#piggybak_order_shipping_address_attributes_state_id,#piggybak_order_shipping_address_attributes_country_id,#piggybak_order_shipping_address_attributes_zip');
9
10
  piggybak.initialize_listeners();
10
11
  piggybak.update_shipping_options($('#piggybak_order_shipping_address_attributes_state_id'), function() {
@@ -14,6 +15,12 @@ $(function() {
14
15
  });
15
16
 
16
17
  var piggybak = {
18
+ prevent_double_click: function() {
19
+ $('#new_piggybak_order').find('input:submit').removeAttr('disabled');
20
+ $('#new_piggybak_order').submit(function() {
21
+ $(this).find('input:submit').attr('disabled', 'disabled');
22
+ });
23
+ },
17
24
  initialize_listeners: function() {
18
25
  shipping_els.live('change', function() {
19
26
  piggybak.update_shipping_options($(this));
@@ -117,6 +124,9 @@ var piggybak = {
117
124
  }
118
125
  $('#shipping_total').html('$' + shipping_total.toFixed(2));
119
126
  var order_total = subtotal + tax_total + shipping_total;
127
+ $.each($('.extra_totals'), function(i, el) {
128
+ order_total += parseFloat($(el).html().replace(/\$/, ''));
129
+ });
120
130
  $('#order_total').html('$' + order_total.toFixed(2));
121
131
  },
122
132
  retrieve_shipping_data: function() {
@@ -1,10 +1,5 @@
1
1
  var geodata;
2
2
 
3
- $(function() {
4
- piggybak_states.populate_geodata();
5
- piggybak_states.initialize_listeners();
6
- });
7
-
8
3
  var piggybak_states = {
9
4
  initialize_listeners: function() {
10
5
  $('#piggybak_order_shipping_address_attributes_country_id').change(function() {
@@ -53,3 +48,9 @@ var piggybak_states = {
53
48
  return;
54
49
  }
55
50
  };
51
+
52
+ $(function() {
53
+ piggybak_states.populate_geodata();
54
+ piggybak_states.initialize_listeners();
55
+ });
56
+
@@ -10,12 +10,14 @@ module Piggybak
10
10
  begin
11
11
  ActiveRecord::Base.transaction do
12
12
  @order = Piggybak::Order.new(params[:piggybak_order])
13
+ @order.create_payment_shipment
13
14
 
14
15
  if Piggybak.config.logging
15
- clean_params = params[:piggybak_order].clone
16
- clean_params["payments_attributes"]["0"]["number"] = clean_params["payments_attributes"]["0"]["number"].mask_cc_number
17
- clean_params["payments_attributes"]["0"]["verification_value"] = clean_params["payments_attributes"]["0"]["verification_value"].mask_csv
18
- logger.info "#{request.remote_ip}:#{Time.now.strftime("%Y-%m-%d %H:%M")} Order received with params #{clean_params.inspect}"
16
+ # TODO: Reimplement on correctly filtered params
17
+ #clean_params = params[:piggybak_order].clone
18
+ #clean_params["payments_attributes"]["0"]["number"] = clean_params["payments_attributes"]["0"]["number"].mask_cc_number
19
+ #clean_params["payments_attributes"]["0"]["verification_value"] = clean_params["payments_attributes"]["0"]["verification_value"].mask_csv
20
+ #logger.info "#{request.remote_ip}:#{Time.now.strftime("%Y-%m-%d %H:%M")} Order received with params #{clean_params.inspect}"
19
21
  end
20
22
  @order.initialize_user(current_user, true)
21
23
 
@@ -28,6 +30,7 @@ module Piggybak
28
30
  end
29
31
 
30
32
  if @order.save
33
+ # TODO: Imporant: figure out how to have notifications not trigger rollback here. Instead log failed order notification sent.
31
34
  Piggybak::Notifier.order_notification(@order).deliver
32
35
 
33
36
  if Piggybak.config.logging
@@ -52,8 +55,9 @@ module Piggybak
52
55
  @order.errors[:base] << "Your order could not go through. Please try again."
53
56
  end
54
57
  end
55
- else
58
+ else
56
59
  @order = Piggybak::Order.new
60
+ @order.create_payment_shipment
57
61
  @order.initialize_user(current_user, false)
58
62
  end
59
63
  end
@@ -95,18 +99,6 @@ module Piggybak
95
99
  redirect_to rails_admin.edit_path('Piggybak::Order', order.id)
96
100
  end
97
101
 
98
- def restore
99
- order = Order.find(params[:id])
100
- order.recorded_changer = current_user.id
101
-
102
- if can?(:restore, order)
103
- order.status = "new"
104
- order.save
105
- end
106
-
107
- redirect_to rails_admin.edit_path('Piggybak::Order', order.id)
108
- end
109
-
110
102
  def cancel
111
103
  order = Order.find(params[:id])
112
104
 
@@ -115,18 +107,18 @@ module Piggybak
115
107
  order.disable_order_notes = true
116
108
 
117
109
  order.line_items.each do |line_item|
118
- line_item.mark_for_destruction
119
- end
120
- order.shipments.each do |shipment|
121
- shipment.mark_for_destruction
110
+ if line_item.line_item_type != "payment"
111
+ line_item.mark_for_destruction
112
+ end
122
113
  end
123
- order.update_attribute(:tax_charge, 0.00)
124
114
  order.update_attribute(:total, 0.00)
125
115
  order.update_attribute(:to_be_cancelled, true)
126
116
 
127
117
  OrderNote.create(:order_id => order.id, :note => "Order set to cancelled. Line items, shipments, tax removed.", :user_id => current_user.id)
128
118
 
129
119
  flash[:notice] = "Order #{order.id} set to cancelled. Order is now in unbalanced state."
120
+ else
121
+ flash[:error] = "You do not have permission to cancel this order."
130
122
  end
131
123
 
132
124
  redirect_to rails_admin.edit_path('Piggybak::Order', order.id)
@@ -15,7 +15,10 @@ module Piggybak
15
15
 
16
16
  after_initialize :set_default_country
17
17
  after_save :document_address_changes
18
-
18
+
19
+ attr_accessible :firstname, :lastname, :address1, :location,
20
+ :address2, :city, :state_id, :zip, :country_id
21
+
19
22
  def set_default_country
20
23
  self.country ||= Country.find_by_abbr(Piggybak.config.default_country)
21
24
  end
@@ -1,29 +1,5 @@
1
1
  module Piggybak
2
2
  class Adjustment < ActiveRecord::Base
3
- belongs_to :order
4
- belongs_to :source, :polymorphic => true
5
- attr_accessor :user_id
6
- acts_as_changer
7
-
8
- validates_presence_of :order_id, :total
9
- validates_numericality_of :total
10
- validates_presence_of :source
11
-
12
- before_validation :set_source
13
-
14
- def set_source
15
- if self.source.nil? && self.user_id.present?
16
- self.source = User.find(self.user_id)
17
- end
18
- end
19
-
20
- def admin_label
21
- if !self.new_record?
22
- return "Adjustment ##{self.id} (#{self.created_at.strftime("%m-%d-%Y")}): " +
23
- "$#{"%.2f" % self.total}"
24
- else
25
- return ""
26
- end
27
- end
3
+ #TODO: Note this is deprecated after 0.5.5
28
4
  end
29
5
  end
@@ -4,18 +4,19 @@ module Piggybak
4
4
  attr_accessor :total
5
5
  attr_accessor :errors
6
6
  attr_accessor :extra_data
7
+ alias :subtotal :total
7
8
 
8
9
  def initialize(cookie='')
9
10
  self.items = []
10
11
  self.errors = []
11
12
  cookie ||= ''
12
13
  cookie.split(';').each do |item|
13
- item_variant = Piggybak::Variant.find_by_id(item.split(':')[0])
14
- if item_variant.present?
15
- self.items << { :variant => item_variant, :quantity => (item.split(':')[1]).to_i }
14
+ item_sellable = Piggybak::Sellable.find_by_id(item.split(':')[0])
15
+ if item_sellable.present?
16
+ self.items << { :sellable => item_sellable, :quantity => (item.split(':')[1]).to_i }
16
17
  end
17
18
  end
18
- self.total = self.items.sum { |item| item[:quantity]*item[:variant].price }
19
+ self.total = self.items.sum { |item| item[:quantity]*item[:sellable].price }
19
20
 
20
21
  self.extra_data = {}
21
22
  end
@@ -38,14 +39,14 @@ module Piggybak
38
39
 
39
40
  def self.add(cookie, params)
40
41
  cart = to_hash(cookie)
41
- cart["#{params[:variant_id]}"] ||= 0
42
- cart["#{params[:variant_id]}"] += params[:quantity].to_i
42
+ cart["#{params[:sellable_id]}"] ||= 0
43
+ cart["#{params[:sellable_id]}"] += params[:quantity].to_i
43
44
  to_string(cart)
44
45
  end
45
46
 
46
- def self.remove(cookie, variant_id)
47
+ def self.remove(cookie, sellable_id)
47
48
  cart = to_hash(cookie)
48
- cart[variant_id] = 0
49
+ cart[sellable_id] = 0
49
50
  to_string(cart)
50
51
  end
51
52
 
@@ -58,7 +59,7 @@ module Piggybak
58
59
  def to_cookie
59
60
  cookie = ''
60
61
  self.items.each do |item|
61
- cookie += "#{item[:variant].id.to_s}:#{item[:quantity].to_s};" if item[:quantity].to_i > 0
62
+ cookie += "#{item[:sellable].id.to_s}:#{item[:quantity].to_s};" if item[:quantity].to_i > 0
62
63
  end
63
64
  cookie
64
65
  end
@@ -67,20 +68,20 @@ module Piggybak
67
68
  self.errors = []
68
69
  new_items = []
69
70
  self.items.each do |item|
70
- if !item[:variant].active
71
- self.errors << ["Sorry, #{item[:variant].description} is no longer for sale"]
72
- elsif item[:variant].unlimited_inventory || item[:variant].quantity >= item[:quantity]
71
+ if !item[:sellable].active
72
+ self.errors << ["Sorry, #{item[:sellable].description} is no longer for sale"]
73
+ elsif item[:sellable].unlimited_inventory || item[:sellable].quantity >= item[:quantity]
73
74
  new_items << item
74
- elsif item[:variant].quantity == 0
75
- self.errors << ["Sorry, #{item[:variant].description} is no longer available"]
75
+ elsif item[:sellable].quantity == 0
76
+ self.errors << ["Sorry, #{item[:sellable].description} is no longer available"]
76
77
  else
77
- self.errors << ["Sorry, only #{item[:variant].quantity} available for #{item[:variant].description}"]
78
- item[:quantity] = item[:variant].quantity
78
+ self.errors << ["Sorry, only #{item[:sellable].quantity} available for #{item[:sellable].description}"]
79
+ item[:quantity] = item[:sellable].quantity
79
80
  new_items << item if item[:quantity] > 0
80
81
  end
81
82
  end
82
83
  self.items = new_items
83
- self.total = self.items.sum { |item| item[:quantity]*item[:variant].price }
84
+ self.total = self.items.sum { |item| item[:quantity]*item[:sellable].price }
84
85
  end
85
86
 
86
87
  def set_extra_data(form_params)
@@ -2,39 +2,126 @@ module Piggybak
2
2
  class LineItem < ActiveRecord::Base
3
3
  belongs_to :order
4
4
  acts_as_changer
5
- belongs_to :variant
6
-
7
- validates_presence_of :variant_id
8
- validates_presence_of :total
9
- validates_presence_of :price
10
- validates_presence_of :description
11
- validates_presence_of :quantity
5
+ belongs_to :sellable
6
+
7
+ validates_presence_of :price, :description, :quantity
12
8
  validates_numericality_of :quantity, :only_integer => true, :greater_than_or_equal_to => 0
13
9
 
14
- after_create :decrease_inventory, :if => Proc.new { |line_item| !line_item.variant.unlimited_inventory }
15
- after_destroy :increase_inventory, :if => Proc.new { |line_item| !line_item.variant.unlimited_inventory }
16
- after_update :update_inventory, :if => Proc.new { |line_item| !line_item.variant.unlimited_inventory }
17
-
10
+ after_create :decrease_inventory, :if => Proc.new { |line_item| line_item.line_item_type == 'sellable' && !line_item.sellable.unlimited_inventory }
11
+ after_destroy :increase_inventory, :if => Proc.new { |line_item| line_item.line_item_type == 'sellable' && !line_item.sellable.unlimited_inventory }
12
+ after_update :update_inventory, :if => Proc.new { |line_item| line_item.line_item_type == 'sellable' && !line_item.sellable.unlimited_inventory }
13
+
14
+ attr_accessible :sellable_id, :price, :unit_price, :description, :quantity, :line_item_type
15
+
16
+ after_initialize :initialize_line_item
17
+ before_validation :preprocess
18
+ before_destroy :destroy_associated_item
19
+
20
+ def initialize_line_item
21
+ self.quantity ||= 1
22
+ self.price ||= 0
23
+ end
24
+
25
+ def preprocess
26
+ # TODO: Investigate if this is unnecessary if you use reject_if on accepts_nested_attributes_for
27
+ Piggybak.config.line_item_types.each do |k, v|
28
+ if v.has_key?(:nested_attrs) && k != self.line_item_type.to_sym
29
+ self.send("#{k}=", nil)
30
+ end
31
+ end
32
+
33
+ method = "preprocess_#{self.line_item_type}"
34
+ self.send(method) if self.respond_to?(method)
35
+ end
36
+
37
+ def preprocess_sellable
38
+ sellable = Piggybak::Sellable.find(self.sellable_id)
39
+
40
+ return if sellable.nil?
41
+
42
+ self.description = sellable.description
43
+ self.unit_price = sellable.price
44
+ self.price = self.unit_price*self.quantity.to_i
45
+ end
46
+
47
+ def preprocess_shipment
48
+ if !self._destroy
49
+ if (self.new_record? || self.shipment.status != 'shipped') && self.shipment && self.shipment.shipping_method
50
+ calculator = self.shipment.shipping_method.klass.constantize
51
+ self.price = calculator.rate(self.shipment.shipping_method, self)
52
+ self.price = ((self.price*100).to_i).to_f/100
53
+ self.description = self.shipment.shipping_method.description
54
+ end
55
+ if self.shipment.nil? || self.shipment.shipping_method.nil?
56
+ self.price = 0.00
57
+ self.description = "Shipping"
58
+ end
59
+ end
60
+ end
61
+
62
+ def preprocess_payment
63
+ if self.new_record?
64
+ self.payment.payment_method_id ||= Piggybak::PaymentMethod.find_by_active(true).id if self.payment
65
+ self.description = "Payment"
66
+ self.price = 0
67
+ end
68
+ end
69
+
70
+ def postprocess_payment
71
+ return true if !self.new_record?
72
+
73
+ if self.payment.process(self.order)
74
+ self.price = -1*self.order.total_due
75
+ self.order.total_due = 0
76
+ return true
77
+ else
78
+ return false
79
+ end
80
+ end
81
+
82
+ # Dependent destroy is not working as expected, so this is in place
83
+ def destroy_associated_item
84
+ line_item_type_sym = self.line_item_type.to_sym
85
+ if Piggybak.config.line_item_types[line_item_type_sym].has_key?(:nested_attrs)
86
+ if Piggybak.config.line_item_types[line_item_type_sym][:nested_attrs]
87
+ b = self.send("#{line_item_type_sym}")
88
+ b.destroy if b.present?
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.line_item_type_select
94
+ Piggybak.config.line_item_types.select { |k, v| v[:visible] }.collect { |k, v| [k.to_s.humanize.titleize, k] }
95
+ end
96
+
97
+ def sellable_id_enum
98
+ ::Piggybak::Sellable.all.collect { |s| ["#{s.description}: $#{s.price}", s.id ] }
99
+ end
100
+
18
101
  def admin_label
19
- "#{self.quantity} x #{self.variant.description}"
102
+ if self.line_item_type == 'sellable'
103
+ "#{self.quantity} x #{self.description} ($#{sprintf("%.2f", self.unit_price)}): $#{sprintf("%.2f", self.price)}".gsub('"', '&quot;')
104
+ else
105
+ "#{self.description}: $#{sprintf("%.2f", self.price)}".gsub('"', '&quot;')
106
+ end
20
107
  end
21
108
 
22
109
  def decrease_inventory
23
- self.variant.update_inventory(-1 * self.quantity)
110
+ self.sellable.update_inventory(-1 * self.quantity)
24
111
  end
25
112
 
26
113
  def increase_inventory
27
- self.variant.update_inventory(self.quantity)
114
+ self.sellable.update_inventory(self.quantity)
28
115
  end
29
116
 
30
117
  def update_inventory
31
- if self.variant_id != self.variant_id_was
32
- old_variant = Variant.find(self.variant_id_was)
33
- old_variant.update_inventory(self.quantity_was)
34
- self.variant.update_inventory(-1*self.quantity)
118
+ if self.sellable_id != self.sellable_id_was
119
+ old_sellable = Sellable.find(self.sellable_id_was)
120
+ old_sellable.update_inventory(self.quantity_was)
121
+ self.sellable.update_inventory(-1*self.quantity)
35
122
  else
36
123
  quantity_diff = self.quantity_was - self.quantity
37
- self.variant.update_inventory(quantity_diff)
124
+ self.sellable.update_inventory(quantity_diff)
38
125
  end
39
126
  end
40
127
  end