piggybak 0.1.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.
- data/.document +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +18 -0
- data/README.rdoc +19 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/app/assets/javascripts/piggybak.js +72 -0
- data/app/controllers/piggybak/cart_controller.rb +29 -0
- data/app/controllers/piggybak/orders_controller.rb +86 -0
- data/app/mailers/piggybak/notifier.rb +10 -0
- data/app/models/piggybak/address.rb +23 -0
- data/app/models/piggybak/cart.rb +77 -0
- data/app/models/piggybak/line_item.rb +31 -0
- data/app/models/piggybak/order.rb +141 -0
- data/app/models/piggybak/payment.rb +80 -0
- data/app/models/piggybak/payment_calculator.rb +7 -0
- data/app/models/piggybak/payment_calculator/authorize_net.rb +6 -0
- data/app/models/piggybak/payment_calculator/fake.rb +24 -0
- data/app/models/piggybak/payment_method.rb +40 -0
- data/app/models/piggybak/payment_method_value.rb +11 -0
- data/app/models/piggybak/product.rb +19 -0
- data/app/models/piggybak/shipment.rb +21 -0
- data/app/models/piggybak/shipping_calculator.rb +12 -0
- data/app/models/piggybak/shipping_calculator/flat_rate.rb +13 -0
- data/app/models/piggybak/shipping_calculator/pickup.rb +23 -0
- data/app/models/piggybak/shipping_calculator/range.rb +16 -0
- data/app/models/piggybak/shipping_method.rb +55 -0
- data/app/models/piggybak/shipping_method_value.rb +11 -0
- data/app/models/piggybak/state.rb +4 -0
- data/app/models/piggybak/tax_calculator.rb +7 -0
- data/app/models/piggybak/tax_calculator/flat_rate.rb +33 -0
- data/app/models/piggybak/tax_method.rb +43 -0
- data/app/models/piggybak/tax_method_value.rb +11 -0
- data/app/views/piggybak/cart/_form.html.erb +18 -0
- data/app/views/piggybak/cart/_items.html.erb +68 -0
- data/app/views/piggybak/cart/show.html.erb +13 -0
- data/app/views/piggybak/notifier/order_notification.text.erb +11 -0
- data/app/views/piggybak/orders/_address_form.html.erb +24 -0
- data/app/views/piggybak/orders/list.html.erb +12 -0
- data/app/views/piggybak/orders/receipt.html.erb +53 -0
- data/app/views/piggybak/orders/show.html.erb +112 -0
- data/app/views/rails_admin/main/_actions.html.erb +5 -0
- data/config/routes.rb +15 -0
- data/db/migrate/20111227150106_create_orders.rb +21 -0
- data/db/migrate/20111227150322_create_addresses.rb +15 -0
- data/db/migrate/20111227150432_create_line_items.rb +10 -0
- data/db/migrate/20111227213558_create_products.rb +14 -0
- data/db/migrate/20111228231756_create_shipping_methods.rb +9 -0
- data/db/migrate/20111228231806_create_payment_methods.rb +11 -0
- data/db/migrate/20111228231829_create_payments.rb +17 -0
- data/db/migrate/20111228231838_create_shipments.rb +12 -0
- data/db/migrate/20111228235852_create_shipping_method_values.rb +9 -0
- data/db/migrate/20111228235853_create_payment_method_values.rb +9 -0
- data/db/migrate/20120102154050_create_tax_methods.rb +9 -0
- data/db/migrate/20120102162415_create_states.rb +8 -0
- data/db/migrate/20120102162703_create_tax_method_values.rb +9 -0
- data/lib/acts_as_orderer/base.rb +29 -0
- data/lib/acts_as_product/base.rb +32 -0
- data/lib/application_helper.rb +12 -0
- data/lib/currency.rb +5 -0
- data/lib/piggybak.rb +295 -0
- data/piggybak.gemspec +115 -0
- metadata +213 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class Order < ActiveRecord::Base
|
3
|
+
has_many :line_items, :inverse_of => :order
|
4
|
+
has_many :payments, :inverse_of => :order
|
5
|
+
has_many :shipments, :inverse_of => :order
|
6
|
+
belongs_to :billing_address, :class_name => "Piggybak::Address"
|
7
|
+
belongs_to :shipping_address, :class_name => "Piggybak::Address"
|
8
|
+
belongs_to :user
|
9
|
+
|
10
|
+
accepts_nested_attributes_for :billing_address, :allow_destroy => true
|
11
|
+
accepts_nested_attributes_for :shipping_address, :allow_destroy => true
|
12
|
+
accepts_nested_attributes_for :shipments, :allow_destroy => true
|
13
|
+
accepts_nested_attributes_for :line_items, :allow_destroy => true
|
14
|
+
accepts_nested_attributes_for :payments
|
15
|
+
|
16
|
+
validates_presence_of :status
|
17
|
+
validates_presence_of :email
|
18
|
+
validates_presence_of :phone
|
19
|
+
validates_presence_of :total
|
20
|
+
validates_presence_of :total_due
|
21
|
+
validates_presence_of :tax_charge
|
22
|
+
validates_presence_of :created_at
|
23
|
+
|
24
|
+
before_validation :set_defaults
|
25
|
+
after_validation :update_totals
|
26
|
+
before_save :process_payments, :update_status
|
27
|
+
|
28
|
+
def process_payments
|
29
|
+
has_errors = false
|
30
|
+
self.payments.each do |payment|
|
31
|
+
if(!payment.process)
|
32
|
+
has_errors = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
self.total_due = self.total
|
37
|
+
payments.each do |payment|
|
38
|
+
self.total_due -= payment.total
|
39
|
+
end
|
40
|
+
!has_errors
|
41
|
+
end
|
42
|
+
|
43
|
+
def details
|
44
|
+
if !self.new_record?
|
45
|
+
subtotal = self.line_items.inject(0) { |subtotal, li| subtotal + li.total }
|
46
|
+
shipping = self.shipments.inject(0) { |shipping, shipment| shipping + shipment.total }
|
47
|
+
return "Status: #{self.status}<br />" +
|
48
|
+
"Subtotal: $#{"%.2f" % subtotal}<br />" +
|
49
|
+
"Shipping: $#{"%.2f" % shipping}<br />" +
|
50
|
+
"Tax: $#{"%.2f" % self.tax_charge}<br />" +
|
51
|
+
"Due: $#{"%.2f" % self.total_due}<br />" +
|
52
|
+
"Created at: #{self.created_at.strftime("%m-%d-%Y")}<br />" #details here"
|
53
|
+
else
|
54
|
+
return "New Order"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_line_items(cart)
|
59
|
+
cart.update_quantities
|
60
|
+
cart.items.each do |item|
|
61
|
+
line_item = Piggybak::LineItem.new({ :product_id => item[:product].id,
|
62
|
+
:total => item[:product].price*item[:quantity],
|
63
|
+
:quantity => item[:quantity] })
|
64
|
+
self.line_items << line_item
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_defaults
|
69
|
+
self.created_at ||= Time.now
|
70
|
+
self.status ||= "new"
|
71
|
+
self.total = 0
|
72
|
+
self.total_due = 0
|
73
|
+
self.tax_charge = 0
|
74
|
+
|
75
|
+
self.line_items.each do |line_item|
|
76
|
+
line_item.total = line_item.product.price * line_item.quantity
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_totals
|
81
|
+
self.total = 0
|
82
|
+
|
83
|
+
self.line_items.each do |line_item|
|
84
|
+
self.total += line_item.total
|
85
|
+
end
|
86
|
+
|
87
|
+
self.tax_charge = TaxMethod.calculate_tax(self)
|
88
|
+
self.total += self.tax_charge
|
89
|
+
|
90
|
+
shipments.each do |shipment|
|
91
|
+
if shipment.new_record?
|
92
|
+
calculator = shipment.shipping_method.klass.constantize
|
93
|
+
shipment.total = calculator.rate(shipment.shipping_method, self)
|
94
|
+
end
|
95
|
+
logger.warn "steph shipment total is #{shipment.total}"
|
96
|
+
self.total += shipment.total
|
97
|
+
end
|
98
|
+
logger.warn "steph: total is #{self.total}"
|
99
|
+
self.total = self.total.to_c
|
100
|
+
|
101
|
+
self.total_due = self.total
|
102
|
+
payments.each do |payment|
|
103
|
+
self.total_due -= payment.total
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def update_status
|
108
|
+
if self.total_due > 0.00
|
109
|
+
self.status = "payment owed"
|
110
|
+
elsif self.total_due < 0.00
|
111
|
+
self.status = "credit_owed"
|
112
|
+
else
|
113
|
+
if self.total == 0.00
|
114
|
+
self.status = "new"
|
115
|
+
elsif self.shipments.all? { |s| s.status == "shipped" }
|
116
|
+
self.status = "shipped"
|
117
|
+
else
|
118
|
+
self.status = "paid"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def status_enum
|
124
|
+
["incomplete", "paid", "shipped"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def avs_address
|
128
|
+
{
|
129
|
+
:address1 => self.billing_address.address1,
|
130
|
+
:city => self.billing_address.city,
|
131
|
+
:state => self.billing_address.state,
|
132
|
+
:zip => self.billing_address.zip,
|
133
|
+
:country => "US"
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
def admin_label
|
138
|
+
"Order ##{self.id}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class Payment < ActiveRecord::Base
|
3
|
+
belongs_to :order
|
4
|
+
belongs_to :payment_method
|
5
|
+
|
6
|
+
validates_presence_of :status
|
7
|
+
validates_presence_of :total
|
8
|
+
validates_presence_of :payment_method_id
|
9
|
+
validates_presence_of :month
|
10
|
+
validates_presence_of :year
|
11
|
+
|
12
|
+
def status_enum
|
13
|
+
["paid"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def month_enum
|
17
|
+
1.upto(12)
|
18
|
+
end
|
19
|
+
|
20
|
+
def year_enum
|
21
|
+
Time.now.year.upto(Time.now.year + 10)
|
22
|
+
end
|
23
|
+
|
24
|
+
def credit_card
|
25
|
+
{ "number" => self.number,
|
26
|
+
"month" => self.month,
|
27
|
+
"year" => self.year,
|
28
|
+
"verification_value" => self.verification_value,
|
29
|
+
"first_name" => self.order.billing_address.firstname,
|
30
|
+
"last_name" => self.order.billing_address.lastname }
|
31
|
+
end
|
32
|
+
|
33
|
+
def process
|
34
|
+
if self.new_record?
|
35
|
+
payment_gateway = self.payment_method.klass.constantize
|
36
|
+
gateway = payment_gateway::KLASS.new(self.payment_method.key_values)
|
37
|
+
credit_card = ActiveMerchant::Billing::CreditCard.new(self.credit_card)
|
38
|
+
gateway_response = gateway.authorize(self.order.total_due*100, credit_card, :address => self.order.avs_address)
|
39
|
+
if gateway_response.success?
|
40
|
+
self.attributes = { :total => self.order.total_due,
|
41
|
+
:number => 'hidden',
|
42
|
+
:verification_value => 'hidden' }
|
43
|
+
gateway.capture(1000, gateway_response.authorization)
|
44
|
+
return true
|
45
|
+
else
|
46
|
+
self.errors.add :payment_method_id, gateway_response.message
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
else
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def admin_label
|
55
|
+
if !self.new_record?
|
56
|
+
return "Payment ##{self.id} (#{self.created_at.strftime("%m-%d-%Y")})<br />" +
|
57
|
+
"Payment Method: #{self.payment_method.description}<br />" +
|
58
|
+
"Status: #{self.status}<br />" +
|
59
|
+
"$#{"%.2f" % self.total}"
|
60
|
+
else
|
61
|
+
return ""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias :details :admin_label
|
65
|
+
|
66
|
+
validates_each :payment_method_id do |record, attr, value|
|
67
|
+
if record.new_record?
|
68
|
+
credit_card = ActiveMerchant::Billing::CreditCard.new(record.credit_card)
|
69
|
+
|
70
|
+
if !credit_card.valid?
|
71
|
+
credit_card.errors.each do |key, value|
|
72
|
+
if value.any? && !["first_name", "last_name", "type"].include?(key)
|
73
|
+
record.errors.add key, value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class PaymentCalculator::Fake < PaymentCalculator
|
3
|
+
KEYS = []
|
4
|
+
KLASS = ::Piggybak::PaymentCalculator::Fake
|
5
|
+
|
6
|
+
def self.new(*args)
|
7
|
+
return self
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.authorize(*args)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.success?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.authorization
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.capture(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class PaymentMethod < ActiveRecord::Base
|
3
|
+
has_many :payment_method_values, :dependent => :destroy
|
4
|
+
alias :metadata :payment_method_values
|
5
|
+
|
6
|
+
accepts_nested_attributes_for :payment_method_values, :allow_destroy => true
|
7
|
+
|
8
|
+
validates_presence_of :klass
|
9
|
+
validates_presence_of :description
|
10
|
+
|
11
|
+
def klass_enum
|
12
|
+
#TODO: Troubleshoot use of subclasses here instead
|
13
|
+
[Piggybak::PaymentCalculator::AuthorizeNet,
|
14
|
+
Piggybak::PaymentCalculator::Fake]
|
15
|
+
end
|
16
|
+
|
17
|
+
validates_each :payment_method_values do |record, attr, value|
|
18
|
+
if record.klass
|
19
|
+
payment_method = record.klass.constantize
|
20
|
+
metadata_keys = value.collect { |v| v.key }.sort
|
21
|
+
if payment_method::KEYS.sort != metadata_keys
|
22
|
+
record.errors.add attr, "You must define key values for #{payment_method::KEYS.join(', ')} for this payment method."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
validates_each :active do |record, attr, value|
|
27
|
+
if value && PaymentMethod.find_all_by_active(true).select { |p| p != record }.size > 0
|
28
|
+
record.errors.add attr, "You may only have one active payment method."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def key_values
|
33
|
+
self.metadata.inject({}) { |h, k| h[k.key.to_sym] = k.value; h }
|
34
|
+
end
|
35
|
+
|
36
|
+
def admin_label
|
37
|
+
"#{self.description}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Piggybak::Product < ActiveRecord::Base
|
2
|
+
belongs_to :item, :polymorphic => true, :inverse_of => :piggybak_product
|
3
|
+
attr_accessible :sku, :description, :price, :quantity, :active, :unlimited_inventory, :item_id, :item_type
|
4
|
+
|
5
|
+
validates_presence_of :sku
|
6
|
+
validates_uniqueness_of :sku
|
7
|
+
validates_presence_of :description
|
8
|
+
validates_presence_of :price
|
9
|
+
validates_presence_of :item_type
|
10
|
+
validates_numericality_of :quantity, :only_integer => true, :greater_than_or_equal_to => 0
|
11
|
+
|
12
|
+
def admin_label
|
13
|
+
"Product: #{self.description}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_inventory(purchased)
|
17
|
+
self.update_attribute(:quantity, self.quantity + purchased)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class Shipment < ActiveRecord::Base
|
3
|
+
belongs_to :order
|
4
|
+
belongs_to :shipping_method
|
5
|
+
|
6
|
+
validates_presence_of :status
|
7
|
+
validates_presence_of :total
|
8
|
+
validates_presence_of :shipping_method_id
|
9
|
+
|
10
|
+
def status_enum
|
11
|
+
["new", "processing", "shipped"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def admin_label
|
15
|
+
"Shipment ##{self.id}<br />" +
|
16
|
+
"#{self.shipping_method.description}<br />" +
|
17
|
+
"Status: #{self.status}<br />" +
|
18
|
+
"$#{"%.2f" % self.total}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class ShippingCalculator::FlatRate < ShippingCalculator
|
3
|
+
KEYS = ["rate"]
|
4
|
+
|
5
|
+
def self.available?(*args)
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.rate(method, object)
|
10
|
+
method.metadata.detect { |m| m.key == "rate" }.value.to_f.to_c
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class ShippingCalculator::Pickup < ShippingCalculator
|
3
|
+
KEYS = ["state_abbr", "rate"]
|
4
|
+
|
5
|
+
def self.available?(method, object)
|
6
|
+
abbr = method.metadata.detect { |t| t.key == "state_abbr" }.value
|
7
|
+
|
8
|
+
if object.is_a?(Cart)
|
9
|
+
state = State.find(object.extra_data["state_id"])
|
10
|
+
return true if state.abbr == abbr
|
11
|
+
else
|
12
|
+
if object.billing_address && object.billing_address.state
|
13
|
+
return object.billing_address.state.abbr == abbr
|
14
|
+
end
|
15
|
+
end
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.rate(method, object)
|
20
|
+
method.metadata.detect { |m| m.key == "rate" }.value.to_f.to_c
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class ShipppingCalculator::Range < ShippingCalculator
|
3
|
+
KEYS = ["cost", "upper", "lower"]
|
4
|
+
|
5
|
+
def self.available?(method, object)
|
6
|
+
low_end = method.metadata.detect { |m| m.key == "lower" }.value
|
7
|
+
high_end = method.metadata.detect { |m| m.key == "upper" }.value
|
8
|
+
|
9
|
+
object.total >= low_end.to_f && object.total <= high_end.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.rate(method, object)
|
13
|
+
method.metadata.detect { |m| m.key == "cost" }.value.to_f.to_c
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Piggybak
|
2
|
+
class ShippingMethod < ActiveRecord::Base
|
3
|
+
has_many :shipping_method_values, :dependent => :destroy
|
4
|
+
alias :metadata :shipping_method_values
|
5
|
+
|
6
|
+
validates_presence_of :description
|
7
|
+
validates_presence_of :klass
|
8
|
+
|
9
|
+
accepts_nested_attributes_for :shipping_method_values, :allow_destroy => true
|
10
|
+
|
11
|
+
validates_each :shipping_method_values do |record, attr, value|
|
12
|
+
if record.klass
|
13
|
+
calculator = record.klass.constantize
|
14
|
+
metadata_keys = value.collect { |v| v.key }.sort
|
15
|
+
if calculator::KEYS.sort != metadata_keys
|
16
|
+
record.errors.add attr, "You must define key values for #{calculator::KEYS.join(', ')} for this shipping method."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def klass_enum
|
22
|
+
#TODO: Troubleshoot use of subclasses here instead
|
23
|
+
[Piggybak::ShippingCalculator::FlatRate,
|
24
|
+
Piggybak::ShippingCalculator::Range,
|
25
|
+
Piggybak::ShippingCalculator::Pickup]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.available_methods(cart)
|
29
|
+
active_methods = ShippingMethod.find_all_by_active(true)
|
30
|
+
|
31
|
+
active_methods.select { |method| method.klass.constantize.available?(method, cart) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.lookup_methods(cart)
|
35
|
+
active_methods = ShippingMethod.find_all_by_active(true)
|
36
|
+
|
37
|
+
active_methods.inject([]) do |arr, method|
|
38
|
+
klass = method.klass.constantize
|
39
|
+
logger.warn "steph: inside here!! #{method.inspect}"
|
40
|
+
if klass.available?(method, cart)
|
41
|
+
rate = klass.rate(method, cart)
|
42
|
+
logger.warn "steph rate is #{rate.inspect}"
|
43
|
+
arr << {
|
44
|
+
:label => "#{method.description} $#{"%.2f" % rate}",
|
45
|
+
:id => method.id,
|
46
|
+
:rate => rate }
|
47
|
+
end
|
48
|
+
arr
|
49
|
+
end
|
50
|
+
end
|
51
|
+
def admin_label
|
52
|
+
self.description
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|