piggybak 0.5.5 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +32 -25
- data/VERSION +1 -1
- data/app/assets/javascripts/piggybak.js +11 -1
- data/app/assets/javascripts/piggybak.states.js +6 -5
- data/app/controllers/piggybak/orders_controller.rb +14 -22
- data/app/models/piggybak/address.rb +4 -1
- data/app/models/piggybak/adjustment.rb +1 -25
- data/app/models/piggybak/cart.rb +18 -17
- data/app/models/piggybak/line_item.rb +106 -19
- data/app/models/piggybak/order.rb +94 -108
- data/app/models/piggybak/payment.rb +25 -27
- data/app/models/piggybak/payment_method.rb +3 -1
- data/app/models/piggybak/payment_method_value.rb +3 -1
- data/app/models/piggybak/{variant.rb → sellable.rb} +6 -4
- data/app/models/piggybak/shipment.rb +4 -10
- data/app/models/piggybak/shipping_calculator/flat_rate.rb +4 -0
- data/app/models/piggybak/shipping_calculator/free.rb +4 -0
- data/app/models/piggybak/shipping_calculator/range.rb +4 -0
- data/app/models/piggybak/shipping_method.rb +1 -1
- data/app/models/piggybak/tax_method.rb +1 -1
- data/app/views/piggybak/cart/_form.html.erb +7 -7
- data/app/views/piggybak/cart/_items.html.erb +14 -5
- data/app/views/piggybak/notifier/order_notification.text.erb +10 -2
- data/app/views/piggybak/orders/_details.html.erb +14 -5
- data/app/views/piggybak/orders/_google_analytics.html.erb +3 -3
- data/app/views/piggybak/orders/download.text.erb +6 -6
- data/app/views/piggybak/orders/submit.html.erb +55 -49
- data/app/views/rails_admin/main/_location_select.html.haml +3 -3
- data/app/views/rails_admin/main/_order_details.html.erb +12 -24
- data/app/views/rails_admin/main/_polymorphic_nested.html.haml +29 -0
- data/bin/piggybak +12 -0
- data/config/routes.rb +0 -2
- data/db/migrate/20121008160425_rename_variants_to_sellables.rb +11 -0
- data/db/migrate/20121008175144_line_item_rearchitecture.rb +96 -0
- data/lib/acts_as_sellable.rb +21 -0
- data/lib/formatted_changes.rb +2 -2
- data/lib/piggybak.rb +80 -126
- data/lib/piggybak/cli.rb +81 -0
- data/lib/piggybak/config.rb +21 -0
- data/piggybak.gemspec +10 -7
- data/spec/dummy_app/app/models/image.rb +1 -1
- data/spec/factories.rb +1 -1
- metadata +52 -49
- data/app/controllers/piggybak/payments_controller.rb +0 -14
- data/app/views/rails_admin/main/_order_notes.html.erb +0 -1
- data/app/views/rails_admin/main/_payment_refund.html.haml +0 -6
- 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
|
-
|
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, :
|
18
|
+
validates_presence_of :status, :email, :phone, :total, :total_due, :created_at, :ip_address, :user_agent
|
27
19
|
|
28
|
-
after_initialize :
|
29
|
-
|
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
|
-
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
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
|
93
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
136
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
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
|
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.
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
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
|
-
|
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)
|
@@ -1,5 +1,5 @@
|
|
1
|
-
class Piggybak::
|
2
|
-
belongs_to :item, :polymorphic => true, :inverse_of => :
|
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
|
-
"
|
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
|