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