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.
Files changed (63) hide show
  1. data/.document +5 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +18 -0
  4. data/README.rdoc +19 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/app/assets/javascripts/piggybak.js +72 -0
  8. data/app/controllers/piggybak/cart_controller.rb +29 -0
  9. data/app/controllers/piggybak/orders_controller.rb +86 -0
  10. data/app/mailers/piggybak/notifier.rb +10 -0
  11. data/app/models/piggybak/address.rb +23 -0
  12. data/app/models/piggybak/cart.rb +77 -0
  13. data/app/models/piggybak/line_item.rb +31 -0
  14. data/app/models/piggybak/order.rb +141 -0
  15. data/app/models/piggybak/payment.rb +80 -0
  16. data/app/models/piggybak/payment_calculator.rb +7 -0
  17. data/app/models/piggybak/payment_calculator/authorize_net.rb +6 -0
  18. data/app/models/piggybak/payment_calculator/fake.rb +24 -0
  19. data/app/models/piggybak/payment_method.rb +40 -0
  20. data/app/models/piggybak/payment_method_value.rb +11 -0
  21. data/app/models/piggybak/product.rb +19 -0
  22. data/app/models/piggybak/shipment.rb +21 -0
  23. data/app/models/piggybak/shipping_calculator.rb +12 -0
  24. data/app/models/piggybak/shipping_calculator/flat_rate.rb +13 -0
  25. data/app/models/piggybak/shipping_calculator/pickup.rb +23 -0
  26. data/app/models/piggybak/shipping_calculator/range.rb +16 -0
  27. data/app/models/piggybak/shipping_method.rb +55 -0
  28. data/app/models/piggybak/shipping_method_value.rb +11 -0
  29. data/app/models/piggybak/state.rb +4 -0
  30. data/app/models/piggybak/tax_calculator.rb +7 -0
  31. data/app/models/piggybak/tax_calculator/flat_rate.rb +33 -0
  32. data/app/models/piggybak/tax_method.rb +43 -0
  33. data/app/models/piggybak/tax_method_value.rb +11 -0
  34. data/app/views/piggybak/cart/_form.html.erb +18 -0
  35. data/app/views/piggybak/cart/_items.html.erb +68 -0
  36. data/app/views/piggybak/cart/show.html.erb +13 -0
  37. data/app/views/piggybak/notifier/order_notification.text.erb +11 -0
  38. data/app/views/piggybak/orders/_address_form.html.erb +24 -0
  39. data/app/views/piggybak/orders/list.html.erb +12 -0
  40. data/app/views/piggybak/orders/receipt.html.erb +53 -0
  41. data/app/views/piggybak/orders/show.html.erb +112 -0
  42. data/app/views/rails_admin/main/_actions.html.erb +5 -0
  43. data/config/routes.rb +15 -0
  44. data/db/migrate/20111227150106_create_orders.rb +21 -0
  45. data/db/migrate/20111227150322_create_addresses.rb +15 -0
  46. data/db/migrate/20111227150432_create_line_items.rb +10 -0
  47. data/db/migrate/20111227213558_create_products.rb +14 -0
  48. data/db/migrate/20111228231756_create_shipping_methods.rb +9 -0
  49. data/db/migrate/20111228231806_create_payment_methods.rb +11 -0
  50. data/db/migrate/20111228231829_create_payments.rb +17 -0
  51. data/db/migrate/20111228231838_create_shipments.rb +12 -0
  52. data/db/migrate/20111228235852_create_shipping_method_values.rb +9 -0
  53. data/db/migrate/20111228235853_create_payment_method_values.rb +9 -0
  54. data/db/migrate/20120102154050_create_tax_methods.rb +9 -0
  55. data/db/migrate/20120102162415_create_states.rb +8 -0
  56. data/db/migrate/20120102162703_create_tax_method_values.rb +9 -0
  57. data/lib/acts_as_orderer/base.rb +29 -0
  58. data/lib/acts_as_product/base.rb +32 -0
  59. data/lib/application_helper.rb +12 -0
  60. data/lib/currency.rb +5 -0
  61. data/lib/piggybak.rb +295 -0
  62. data/piggybak.gemspec +115 -0
  63. 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,7 @@
1
+ module Piggybak
2
+ class PaymentCalculator
3
+ def self.authorize(*args)
4
+ self::KLASS.authorize(*args)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Piggybak
2
+ class PaymentCalculator::AuthorizeNet < PaymentCalculator
3
+ KEYS = ["login", "password"]
4
+ KLASS = ::ActiveMerchant::Billing::AuthorizeNetGateway
5
+ end
6
+ 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,11 @@
1
+ module Piggybak
2
+ class PaymentMethodValue < ActiveRecord::Base
3
+ belongs_to :payment_method
4
+ validates_presence_of :key
5
+ validates_presence_of :value
6
+
7
+ def admin_label
8
+ "#{self.key} - #{self.value}"
9
+ end
10
+ end
11
+ 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,12 @@
1
+ module Piggybak
2
+ class ShippingCalculator
3
+ def self.available?(*args)
4
+ false
5
+ end
6
+
7
+ def self.lookup(*args)
8
+ { :available => false,
9
+ :rate => 0.00 }
10
+ end
11
+ end
12
+ 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