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
@@ -1,9 +1,6 @@
1
1
  module Piggybak
2
2
  class Order < ActiveRecord::Base
3
3
  has_many :line_items, :inverse_of => :order
4
- has_many :payments, :inverse_of => :order
5
- has_many :shipments, :inverse_of => :order
6
- has_many :adjustments, :inverse_of => :order
7
4
  has_many :order_notes, :inverse_of => :order
8
5
 
9
6
  belongs_to :billing_address, :class_name => "Piggybak::Address"
@@ -12,42 +9,38 @@ module Piggybak
12
9
 
13
10
  accepts_nested_attributes_for :billing_address, :allow_destroy => true
14
11
  accepts_nested_attributes_for :shipping_address, :allow_destroy => true
15
- accepts_nested_attributes_for :shipments, :allow_destroy => true
16
12
  accepts_nested_attributes_for :line_items, :allow_destroy => true
17
- accepts_nested_attributes_for :payments
18
- accepts_nested_attributes_for :adjustments, :allow_destroy => true
19
13
  accepts_nested_attributes_for :order_notes
20
14
 
21
- attr_accessor :recorded_changes
22
- attr_accessor :recorded_changer
23
- attr_accessor :was_new_record
24
- attr_accessor :disable_order_notes
15
+ attr_accessor :recorded_changes, :recorded_changer,
16
+ :was_new_record, :disable_order_notes
25
17
 
26
- validates_presence_of :status, :email, :phone, :total, :total_due, :tax_charge, :created_at, :ip_address, :user_agent
18
+ validates_presence_of :status, :email, :phone, :total, :total_due, :created_at, :ip_address, :user_agent
27
19
 
28
- after_initialize :initialize_nested, :initialize_request
29
- before_validation :set_defaults, :prepare_for_destruction
30
- after_validation :update_totals
31
- before_save :process_payments, :update_status, :set_new_record
20
+ after_initialize :initialize_defaults
21
+ before_save :postprocess_order, :update_status, :set_new_record
32
22
  after_save :record_order_note
33
23
 
34
24
  default_scope :order => 'created_at DESC'
35
25
 
36
- def initialize_nested
26
+ attr_accessible :user_id, :email, :phone, :billing_address_attributes,
27
+ :shipping_address_attributes, :line_items_attributes,
28
+ :order_notes_attributes, :details, :recorded_changer, :ip_address
29
+
30
+ def initialize_defaults
37
31
  self.recorded_changes ||= []
38
32
 
39
33
  self.billing_address ||= Piggybak::Address.new
40
34
  self.shipping_address ||= Piggybak::Address.new
41
- self.shipments ||= [Piggybak::Shipment.new]
42
- self.payments ||= [Piggybak::Payment.new]
43
- if self.payments.any?
44
- self.payments.first.payment_method_id = Piggybak::PaymentMethod.find_by_active(true).id
45
- end
46
- end
47
35
 
48
- def initialize_request
49
36
  self.ip_address ||= 'admin'
50
37
  self.user_agent ||= 'admin'
38
+
39
+ self.created_at ||= Time.now
40
+ self.status ||= "new"
41
+ self.total ||= 0
42
+ self.total_due ||= 0
43
+ self.disable_order_notes = false
51
44
  end
52
45
 
53
46
  def initialize_user(user, on_post)
@@ -57,26 +50,62 @@ module Piggybak
57
50
  end
58
51
  end
59
52
 
60
- def process_payments
61
- has_errors = false
62
-
63
- self.payments.each do |payment|
64
- if(!payment.process)
65
- has_errors = true
53
+ def postprocess_order
54
+ # Mark line items for destruction if quantity == 0
55
+ self.line_items.each do |line_item|
56
+ if line_item.quantity == 0
57
+ line_item.mark_for_destruction
66
58
  end
67
59
  end
68
60
 
69
- payments_total = self.payments.inject(0) { |s, payment| s + payment.total }
61
+ # Recalculate and create line item for tax
62
+ # If a tax line item already exists, reset price
63
+ # If a tax line item doesn't, create
64
+ # If tax is 0, destroy tax line item
65
+ tax = TaxMethod.calculate_tax(self)
66
+ tax_line_item = self.line_items.detect { |line_item| line_item.line_item_type == "tax" }
67
+ if tax > 0
68
+ if tax_line_item
69
+ tax_line_item.price = tax
70
+ else
71
+ self.line_items << LineItem.new({ :line_item_type => "tax", :description => "Tax Charge", :price => tax })
72
+ end
73
+ elsif tax_line_item
74
+ tax_line_item.mark_for_destruction
75
+ end
70
76
 
71
- adjustments.each do |adjustment|
72
- if !adjustment._destroy
73
- payments_total += adjustment.total.round(2)
77
+ # Postprocess everything but payments first
78
+ self.line_items.each do |line_item|
79
+ next if line_item.line_item_type == "payment"
80
+ method = "postprocess_#{line_item.line_item_type}"
81
+ if line_item.respond_to?(method)
82
+ if !line_item.send(method)
83
+ return false
84
+ end
74
85
  end
75
86
  end
87
+
88
+ # Recalculating total and total due, in case post process changed totals
89
+ self.total = 0
90
+ self.line_items.each do |line_item|
91
+ if !line_item._destroy
92
+ self.total += line_item.price
93
+ end
94
+ end
95
+ self.total_due = self.total
76
96
 
77
- self.total_due = (self.total - payments_total).round(2)
97
+ # Postprocess payment last
98
+ self.line_items.each do |line_item|
99
+ next if line_item.line_item_type != "payment"
100
+ method = "postprocess_#{line_item.line_item_type}"
101
+ if line_item.respond_to?(method)
102
+ if !line_item.send(method)
103
+ return false
104
+ end
105
+ end
106
+ end
78
107
 
79
- !has_errors
108
+ true
80
109
  end
81
110
 
82
111
  def record_order_note
@@ -89,82 +118,38 @@ module Piggybak
89
118
  end
90
119
  end
91
120
 
92
- def add_line_items(cart)
93
- cart.update_quantities
94
- cart.items.each do |item|
95
- line_item = Piggybak::LineItem.new({ :variant_id => item[:variant].id,
96
- :price => item[:variant].price,
97
- :total => item[:variant].price*item[:quantity],
98
- :description => item[:variant].description,
99
- :quantity => item[:quantity] })
100
- self.line_items << line_item
101
- end
102
- end
103
-
104
- def set_defaults
105
- self.created_at ||= Time.now
106
- self.status ||= "new"
107
- self.total = 0
108
- self.total_due = 0
109
- self.tax_charge = 0
110
- self.disable_order_notes = false
111
-
112
- return if self.to_be_cancelled
121
+ def create_payment_shipment
122
+ shipment_line_item = self.line_items.detect { |li| li.line_item_type == "shipment" }
113
123
 
114
- self.line_items.each do |line_item|
115
- if self.status != "shipped"
116
- line_item.description = line_item.variant.description
117
- line_item.price = line_item.variant.price
118
- end
119
- if line_item.variant
120
- line_item.total = line_item.price * line_item.quantity.to_i
121
- else
122
- line_item.total = 0
123
- end
124
+ if shipment_line_item.nil?
125
+ new_shipment_line_item = Piggybak::LineItem.new({ :line_item_type => "shipment" })
126
+ new_shipment_line_item.build_shipment
127
+ self.line_items << new_shipment_line_item
128
+ elsif shipment_line_item.shipment.nil?
129
+ shipment_line_item.build_shipment
130
+ else
131
+ previous_method = shipment_line_item.shipment.shipping_method_id
132
+ shipment_line_item.build_shipment
133
+ shipment_line_item.shipment.shipping_method_id = previous_method
124
134
  end
125
- end
126
135
 
127
- def prepare_for_destruction
128
- self.line_items.each do |line_item|
129
- if line_item.quantity == 0
130
- line_item.mark_for_destruction
131
- end
136
+ if !self.line_items.detect { |li| li.line_item_type == "payment" }
137
+ payment_line_item = Piggybak::LineItem.new({ :line_item_type => "payment" })
138
+ payment_line_item.build_payment
139
+ self.line_items << payment_line_item
132
140
  end
133
141
  end
134
142
 
135
- def update_totals
136
- self.total = 0
137
-
138
- self.line_items.each do |line_item|
139
- if !line_item._destroy
140
- self.total += line_item.total
141
- end
142
- end
143
-
144
- self.tax_charge = TaxMethod.calculate_tax(self)
145
- self.total += self.tax_charge
146
-
147
- shipments.each do |shipment|
148
- if !shipment._destroy
149
- if (shipment.new_record? || shipment.status != 'shipped') && shipment.shipping_method
150
- calculator = shipment.shipping_method.klass.constantize
151
- shipment.total = calculator.rate(shipment.shipping_method, self)
152
- end
153
-
154
- shipping_cast = ((shipment.total*100).to_i).to_f/100
155
- self.total += shipping_cast
156
- end
157
- end
158
-
159
- payments_total = self.payments.inject(0) { |s, payment| s + payment.total }
143
+ def add_line_items(cart)
144
+ cart.update_quantities
160
145
 
161
- adjustments.each do |adjustment|
162
- if !adjustment._destroy
163
- payments_total += adjustment.total.round(2)
164
- end
146
+ cart.items.each do |item|
147
+ self.line_items << Piggybak::LineItem.new({ :sellable_id => item[:sellable].id,
148
+ :unit_price => item[:sellable].price,
149
+ :price => item[:sellable].price*item[:quantity],
150
+ :description => item[:sellable].description,
151
+ :quantity => item[:quantity] })
165
152
  end
166
-
167
- self.total_due = (self.total - payments_total).round(2)
168
153
  end
169
154
 
170
155
  def update_status
@@ -175,18 +160,19 @@ module Piggybak
175
160
  else
176
161
  if self.to_be_cancelled
177
162
  self.status = "cancelled"
178
- elsif self.shipments.any? && self.shipments.all? { |s| s.status == "shipped" }
163
+ # TODO: line items scope doesn't work here, maybe on new items? Fix if possible.
164
+ elsif line_items.select { |li| li.line_item_type == "shipment" }.any? && line_items.select { |li| li.line_item_type == "shipment" }.all? { |s| s.shipment.status == "shipped" }
179
165
  self.status = "shipped"
180
- elsif self.shipments.any? && self.shipments.all? { |s| s.status == "processing" }
166
+ elsif line_items.select { |li| li.line_item_type == "shipment" }.any? && line_items.select { |li| li.line_item_type == "shipment" }.all? { |s| s.shipment.status == "processing" }
181
167
  self.status = "processing"
182
168
  else
183
169
  self.status = "new"
184
170
  end
185
171
  end
186
172
  end
173
+
187
174
  def set_new_record
188
175
  self.was_new_record = self.new_record?
189
-
190
176
  true
191
177
  end
192
178
 
@@ -211,9 +197,9 @@ module Piggybak
211
197
  def subtotal
212
198
  v = 0
213
199
 
214
- self.line_items.each do |line_item|
200
+ self.line_items.select { |li| li.line_item_type == "sellable" }.each do |line_item|
215
201
  if !line_item._destroy
216
- v += line_item.total
202
+ v += line_item.price
217
203
  end
218
204
  end
219
205
 
@@ -1,11 +1,10 @@
1
1
  module Piggybak
2
2
  class Payment < ActiveRecord::Base
3
3
  belongs_to :order
4
- acts_as_changer
5
4
  belongs_to :payment_method
5
+ belongs_to :line_item
6
6
 
7
7
  validates_presence_of :status
8
- validates_presence_of :total
9
8
  validates_presence_of :payment_method_id
10
9
  validates_presence_of :month
11
10
  validates_presence_of :year
@@ -13,6 +12,9 @@ module Piggybak
13
12
  attr_accessor :number
14
13
  attr_accessor :verification_value
15
14
 
15
+ attr_accessible :number, :verification_value, :month, :year,
16
+ :transaction_id, :masked_number, :payment_method_id
17
+
16
18
  def status_enum
17
19
  ["paid"]
18
20
  end
@@ -30,30 +32,27 @@ module Piggybak
30
32
  "month" => self.month,
31
33
  "year" => self.year,
32
34
  "verification_value" => self.verification_value,
33
- "first_name" => self.order.billing_address.firstname,
34
- "last_name" => self.order.billing_address.lastname }
35
+ "first_name" => self.line_item ? self.line_item.order.billing_address.firstname : nil,
36
+ "last_name" => self.line_item ? self.line_item.order.billing_address.lastname : nil }
35
37
  end
36
38
 
37
- def process
39
+ def process(order)
40
+ return true if !self.new_record?
41
+
38
42
  ActiveMerchant::Billing::Base.mode = Piggybak.config.activemerchant_mode
39
43
 
40
- if self.new_record?
41
- payment_gateway = self.payment_method.klass.constantize
42
- gateway = payment_gateway::KLASS.new(self.payment_method.key_values)
43
- p_credit_card = ActiveMerchant::Billing::CreditCard.new(self.credit_card)
44
- gateway_response = gateway.authorize(self.order.total_due*100, p_credit_card, :address => self.order.avs_address)
45
- if gateway_response.success?
46
- self.attributes = { :total => self.order.total_due,
47
- :transaction_id => payment_gateway.transaction_id(gateway_response),
48
- :masked_number => self.number.mask_cc_number }
49
- gateway.capture(self.order.total_due*100, gateway_response.authorization, { :credit_card => p_credit_card } )
50
- return true
51
- else
52
- self.errors.add :payment_method_id, gateway_response.message
53
- return false
54
- end
55
- else
44
+ payment_gateway = self.payment_method.klass.constantize
45
+ gateway = payment_gateway::KLASS.new(self.payment_method.key_values)
46
+ p_credit_card = ActiveMerchant::Billing::CreditCard.new(self.credit_card)
47
+ gateway_response = gateway.authorize(order.total_due*100, p_credit_card, :address => order.avs_address)
48
+ if gateway_response.success?
49
+ self.attributes = { :transaction_id => payment_gateway.transaction_id(gateway_response),
50
+ :masked_number => self.number.mask_cc_number }
51
+ gateway.capture(order.total_due*100, gateway_response.authorization, { :credit_card => p_credit_card } )
56
52
  return true
53
+ else
54
+ self.errors.add :payment_method_id, gateway_response.message
55
+ return false
57
56
  end
58
57
  end
59
58
 
@@ -66,20 +65,19 @@ module Piggybak
66
65
  return
67
66
  end
68
67
 
69
- def admin_label
68
+ def details
70
69
  if !self.new_record?
71
- return "Payment ##{self.id} (#{self.created_at.strftime("%m-%d-%Y")}): " +
72
- "$#{"%.2f" % self.total}"
70
+ return "Payment ##{self.id} (#{self.created_at.strftime("%m-%d-%Y")}): " #+
71
+ #"$#{"%.2f" % self.total}" reference line item total here instead
73
72
  else
74
73
  return ""
75
74
  end
76
75
  end
77
- alias :details :admin_label
78
76
 
79
77
  validates_each :payment_method_id do |record, attr, value|
80
78
  if record.new_record?
81
- credit_card = ActiveMerchant::Billing::CreditCard.new(record.credit_card)
82
-
79
+ credit_card = ActiveMerchant::Billing::CreditCard.new(record.credit_card)
80
+
83
81
  if !credit_card.valid?
84
82
  credit_card.errors.each do |key, value|
85
83
  if value.any? && !["first_name", "last_name", "type"].include?(key)
@@ -7,7 +7,9 @@ module Piggybak
7
7
 
8
8
  validates_presence_of :klass
9
9
  validates_presence_of :description
10
-
10
+
11
+ attr_accessible :active, :payment_method_values_attributes, :description,
12
+ :klass
11
13
  def klass_enum
12
14
  Piggybak.config.payment_calculators
13
15
  end
@@ -3,7 +3,9 @@ module Piggybak
3
3
  belongs_to :payment_method
4
4
  validates_presence_of :key
5
5
  validates_presence_of :value
6
-
6
+
7
+ attr_accessible :key, :value
8
+
7
9
  def admin_label
8
10
  "#{self.key} - #{self.value}"
9
11
  end
@@ -1,5 +1,5 @@
1
- class Piggybak::Variant < ActiveRecord::Base
2
- belongs_to :item, :polymorphic => true, :inverse_of => :piggybak_variant
1
+ class Piggybak::Sellable < ActiveRecord::Base
2
+ belongs_to :item, :polymorphic => true, :inverse_of => :piggybak_sellable
3
3
  attr_accessible :sku, :description, :price, :quantity, :active, :unlimited_inventory, :item_id, :item_type
4
4
  attr_accessible :item # to allow direct assignment from code or console
5
5
 
@@ -9,9 +9,11 @@ class Piggybak::Variant < ActiveRecord::Base
9
9
  validates_presence_of :price
10
10
  validates_presence_of :item_type
11
11
  validates_numericality_of :quantity, :only_integer => true, :greater_than_or_equal_to => 0
12
-
12
+
13
+ has_many :line_items, :as => :reference, :inverse_of => :reference
14
+
13
15
  def admin_label
14
- "Variant: #{self.description}"
16
+ "Sellable: #{self.description}"
15
17
  end
16
18
 
17
19
  def update_inventory(purchased)
@@ -1,22 +1,16 @@
1
1
  module Piggybak
2
2
  class Shipment < ActiveRecord::Base
3
3
  belongs_to :order
4
- acts_as_changer
5
4
  belongs_to :shipping_method
5
+ belongs_to :line_item
6
6
 
7
7
  validates_presence_of :status
8
- validates_presence_of :total
9
8
  validates_presence_of :shipping_method_id
10
-
9
+
10
+ attr_accessible :shipping_method_id, :status
11
+
11
12
  def status_enum
12
13
  ["new", "processing", "shipped"]
13
14
  end
14
-
15
- def admin_label
16
- "Shipment ##{self.id}<br />" +
17
- "#{self.shipping_method.description}<br />" +
18
- "Status: #{self.status}<br />" +
19
- "$#{"%.2f" % self.total}"
20
- end
21
15
  end
22
16
  end