piggybak 0.5.5 → 0.6.0

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 (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