piggybak 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/Gemfile +2 -0
  2. data/Gemfile.lock +5 -0
  3. data/README.md +117 -0
  4. data/Rakefile +2 -1
  5. data/VERSION +1 -1
  6. data/app/assets/javascripts/piggybak.js +62 -2
  7. data/app/controllers/piggybak/orders_controller.rb +26 -5
  8. data/{lib/application_helper.rb → app/helpers/piggybak_helper.rb} +6 -1
  9. data/app/models/piggybak/address.rb +14 -1
  10. data/app/models/piggybak/cart.rb +19 -12
  11. data/app/models/piggybak/country.rb +5 -0
  12. data/app/models/piggybak/credit.rb +8 -0
  13. data/app/models/piggybak/line_item.rb +13 -6
  14. data/app/models/piggybak/order.rb +12 -6
  15. data/app/models/piggybak/payment.rb +5 -3
  16. data/app/models/piggybak/payment_calculator/authorize_net.rb +4 -0
  17. data/app/models/piggybak/payment_calculator/fake.rb +4 -0
  18. data/app/models/piggybak/payment_method.rb +8 -3
  19. data/app/models/piggybak/shipping_calculator/free.rb +13 -0
  20. data/app/models/piggybak/shipping_calculator/pickup.rb +1 -1
  21. data/app/models/piggybak/shipping_method.rb +9 -7
  22. data/app/models/piggybak/state.rb +1 -0
  23. data/app/models/piggybak/tax_calculator/{flat_rate.rb → percent.rb} +1 -1
  24. data/app/models/piggybak/tax_method.rb +8 -2
  25. data/app/models/piggybak/{product.rb → variant.rb} +5 -4
  26. data/app/views/piggybak/cart/_form.html.erb +7 -7
  27. data/app/views/piggybak/cart/_items.html.erb +9 -9
  28. data/app/views/piggybak/cart/show.html.erb +2 -0
  29. data/app/views/piggybak/notifier/order_notification.text.erb +1 -1
  30. data/app/views/piggybak/orders/_address_form.html.erb +18 -14
  31. data/app/views/piggybak/orders/_details.html.erb +48 -0
  32. data/app/views/piggybak/orders/download.text.erb +19 -0
  33. data/app/views/piggybak/orders/list.html.erb +12 -12
  34. data/app/views/piggybak/orders/no_access.text.erb +1 -0
  35. data/app/views/piggybak/orders/receipt.html.erb +1 -49
  36. data/app/views/piggybak/orders/show.html.erb +36 -31
  37. data/app/views/rails_admin/main/_actions.html.erb +9 -2
  38. data/config/routes.rb +12 -3
  39. data/db/migrate/20111227150106_create_orders.rb +4 -3
  40. data/db/migrate/20111227150322_create_addresses.rb +2 -1
  41. data/db/migrate/20111227150432_create_line_items.rb +2 -2
  42. data/db/migrate/{20111227213558_create_products.rb → 20111227213558_create_variants.rb} +3 -3
  43. data/db/migrate/20111228231829_create_payments.rb +3 -1
  44. data/db/migrate/20111228231838_create_shipments.rb +1 -1
  45. data/db/migrate/20120102162414_create_countries.rb +10 -0
  46. data/db/migrate/20120102162415_create_states.rb +1 -0
  47. data/db/migrate/20120104020930_populate_countries_and_states.rb +18 -0
  48. data/db/migrate/20120106010412_create_credits.rb +14 -0
  49. data/lib/{acts_as_product → acts_as_variant}/base.rb +6 -6
  50. data/lib/piggybak.rb +59 -23
  51. data/piggybak.gemspec +26 -11
  52. metadata +64 -62
  53. data/README.rdoc +0 -19
data/Gemfile CHANGED
@@ -11,3 +11,5 @@ group :development do
11
11
  gem "jeweler", "~> 1.6.4"
12
12
  gem "rcov", ">= 0"
13
13
  end
14
+
15
+ gem "countries"
@@ -1,6 +1,10 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ countries (0.8.1)
5
+ currencies (= 0.4.0)
6
+ currencies (>= 0.2.0)
7
+ currencies (0.4.0)
4
8
  git (1.2.5)
5
9
  jeweler (1.6.4)
6
10
  bundler (~> 1.0)
@@ -14,5 +18,6 @@ PLATFORMS
14
18
 
15
19
  DEPENDENCIES
16
20
  bundler (~> 1.0.0)
21
+ countries
17
22
  jeweler (~> 1.6.4)
18
23
  rcov
@@ -0,0 +1,117 @@
1
+ Piggybak Gem (Engine)
2
+ ========
3
+
4
+ Modular / mountable ecommerce gem. Features:
5
+
6
+ * Configurable tax methods, shipping methods, payment methods
7
+
8
+ * One page checkout, with AJAX for shipping and tax calculations
9
+
10
+ * Order processing completed in transaction, minimizing orphan data created
11
+
12
+ * Fully defined backend RailsAdmin interface for adding orders on the backend
13
+
14
+ This engine explicitly excludes:
15
+
16
+ * SSL configuration (to be handled in your parent application)
17
+
18
+ * Redirects on login / logout (see recipe below)
19
+
20
+ * Coupons and Gift cerficates (May be added later)
21
+
22
+ * Per unit inventory tracking
23
+
24
+ * Downloadable products
25
+
26
+ This engine is highly dependent on:
27
+
28
+ * Rails 3.1 (Assets, Engines)
29
+
30
+ * RailsAdmin (Admin UI)
31
+
32
+ * Devise (User Authentication)
33
+
34
+ * CanCan (User Authorization)
35
+
36
+ Installation
37
+ ========
38
+
39
+ * First, add to Gemfile:
40
+
41
+ gem "piggybak", :git => "git://github.com/stephskardal/demo.git"
42
+
43
+ * Next, run rake task to copy migrations:
44
+
45
+ rake piggybak_engine:install:migrations
46
+
47
+ * Next, run rake task to run migrations:
48
+
49
+ rake db:migrate
50
+
51
+ * Next, mount in your application by adding:
52
+
53
+ mount Piggybak::Engine => '/checkout', :as => 'piggybak'" to config/routes
54
+
55
+ Integration Components
56
+ ========
57
+
58
+ * Add acts_as_variant to models that will be sellable
59
+ * Add acts_as_orderer to user model (or model that devise hooks into as authenticated user)
60
+ * Add <%= cart_form(@some_item) %> to view to display cart form
61
+ * Add <%= cart_link %> to display link to cart
62
+ * Add <%= orders_link %> to display link to user orders
63
+
64
+ Recipes
65
+ ========
66
+
67
+ * Redirect after login / logout
68
+
69
+ before_filter :set_last_page
70
+ def set_last_page
71
+ if !request.xhr? && !request.url.match(/users\/sign_in/) && !request.url.match(/users\/sign_out/)
72
+ session[:return_to] = request.url
73
+ end
74
+ end
75
+ def after_sign_in_path_for(resource_or_scope)
76
+ session[:return_to] || root_url
77
+ end
78
+ def after_sign_out_path_for(resource_or_scope)
79
+ session[:return_to] || root_url
80
+ end
81
+
82
+ * Cancan access control
83
+
84
+ class Ability
85
+ include CanCan::Ability
86
+ def initialize(user)
87
+ if user && user.roles.include?(Role.find_by_name("admin"))
88
+ can :access, :rails_admin
89
+ can :manage, [ #Insert your app models here
90
+ ::Piggybak::Variant,
91
+ ::Piggybak::ShippingMethod,
92
+ ::Piggybak::PaymentMethod,
93
+ ::Piggybak::TaxMethod,
94
+ ::Piggybak::State,
95
+ ::Piggybak::Country]
96
+ can [:download, :email, :read, :create, :update, :history, :export], ::Piggybak::Order
97
+ end
98
+ end
99
+ end
100
+
101
+
102
+ Roadmap / TODOs
103
+ ========
104
+
105
+ * Figure out how to make entire payments section read only, except for ability to refund
106
+ * Add refunds: Add actionable link under payments
107
+ * Handle state options in admin: selected state or free text
108
+
109
+ * Create rake task for copying over views, and make sure app views will override gems
110
+ * Test email send functionality
111
+ * Test a different user model
112
+ * Add unit testing
113
+
114
+ Copyright
115
+ ========
116
+
117
+ Copyright (c) 2011 Steph Skardal. See LICENSE.txt for further details.
data/Rakefile CHANGED
@@ -20,11 +20,12 @@ Jeweler::Tasks.new do |gem|
20
20
  gem.summary = %Q{Mountable ecommerce}
21
21
  gem.description = %Q{Mountable ecommerce}
22
22
  gem.email = "steph@endpoint.com"
23
- gem.authors = ["Steph Skardal"]
23
+ gem.authors = ["Steph Skardal", "Brian Buchalter"]
24
24
 
25
25
  gem.add_dependency "rails_admin"
26
26
  gem.add_dependency "devise"
27
27
  gem.add_dependency "activemerchant"
28
+ gem.add_dependency "countries"
28
29
  end
29
30
  Jeweler::RubygemsDotOrgTasks.new
30
31
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -1,13 +1,54 @@
1
1
  var tax_total = 0;
2
+ var geodata;
2
3
 
3
4
  $(function() {
4
- piggybak.update_shipping_options($('#piggybak_order_shipping_address_attributes_state_id'));
5
+ piggybak.populate_geodata();
5
6
  piggybak.initialize_listeners();
7
+ piggybak.update_shipping_options($('#piggybak_order_shipping_address_attributes_state_id'));
6
8
  piggybak.update_tax();
7
9
  });
8
10
 
9
11
  var piggybak = {
12
+ populate_geodata: function() {
13
+ $.ajax({
14
+ url: geodata_lookup,
15
+ cached: false,
16
+ dataType: "JSON",
17
+ success: function(data) {
18
+ geodata = data;
19
+ }
20
+ });
21
+ },
22
+ update_state_option: function(type, block) {
23
+ var country_field = $('#piggybak_order_' + type + '_address_attributes_country_id');
24
+ var country_id = country_field.val();
25
+ var new_field;
26
+
27
+ if(geodata.countries["country_" + country_id].length > 0) {
28
+ new_field = $('<select>');
29
+ $.each(geodata.countries["country_" + country_id], function(i, j) {
30
+ new_field.append($('<option>').val(j.id).html(j.name));
31
+ });
32
+ } else {
33
+ new_field = $('<input>');
34
+ }
35
+ var old_field = $('#piggybak_order_' + type + '_address_attributes_state_id');
36
+ new_field.attr('name', old_field.attr('name'));
37
+ new_field.attr('id', old_field.attr('id'));
38
+ old_field.replaceWith(new_field);
39
+
40
+ if(block) {
41
+ block();
42
+ }
43
+ return;
44
+ },
10
45
  initialize_listeners: function() {
46
+ $('#piggybak_order_shipping_address_attributes_country_id').change(function() {
47
+ piggybak.update_state_option('shipping');
48
+ });
49
+ $('#piggybak_order_billing_address_attributes_country_id').change(function() {
50
+ piggybak.update_state_option('billing');
51
+ });
11
52
  $('#piggybak_order_shipping_address_attributes_state_id').change(function() {
12
53
  piggybak.update_shipping_options($(this));
13
54
  });
@@ -17,8 +58,24 @@ var piggybak = {
17
58
  $('#shipping select').change(function() {
18
59
  piggybak.update_totals();
19
60
  });
61
+ $('#shipping_address #copy').click(function() {
62
+ piggybak.copy_from_billing();
63
+ return false;
64
+ });
20
65
  return;
21
66
  },
67
+ copy_from_billing: function() {
68
+ $('#billing_address input').each(function(i, j) {
69
+ var id = $(j).attr('id').replace(/billing_address/, 'shipping_address');
70
+ $('#' + id).val($(j).val());
71
+ });
72
+ var country = $('#piggybak_order_billing_address_attributes_country_id').val();
73
+ $('#piggybak_order_shipping_address_attributes_country_id').val(country);
74
+ piggybak.update_state_option('shipping', function() {
75
+ var state = $('#piggybak_order_billing_address_attributes_state_id').val();
76
+ $('#piggybak_order_shipping_address_attributes_state_id').val(state);
77
+ });
78
+ },
22
79
  update_shipping_options: function(field) {
23
80
  var shipping_field = $('#piggybak_order_shipments_attributes_0_shipping_method_id');
24
81
  shipping_field.hide();
@@ -64,7 +121,10 @@ var piggybak = {
64
121
  update_totals: function() {
65
122
  var subtotal = $('#subtotal_total').data('total');
66
123
  $('#tax_total').html('$' + tax_total.toFixed(2));
67
- var shipping_total = $('#shipping select option:selected').data('rate');
124
+ var shipping_total = 0;
125
+ if($('#shipping select option:selected').length) {
126
+ shipping_total = $('#shipping select option:selected').data('rate');
127
+ }
68
128
  $('#shipping_total').html('$' + shipping_total.toFixed(2));
69
129
  var order_total = subtotal + tax_total + shipping_total;
70
130
  $('#order_total').html('$' + order_total.toFixed(2));
@@ -42,7 +42,6 @@ module Piggybak
42
42
  end
43
43
  end
44
44
  rescue Exception => e
45
- @message = e.message
46
45
  @cart = Piggybak::Cart.new(request.cookies["cart"])
47
46
 
48
47
  if current_user
@@ -63,18 +62,31 @@ module Piggybak
63
62
  end
64
63
 
65
64
  def list
66
- @user = current_user
67
- redirect_to root if @user.nil?
65
+ redirect_to root if current_user.nil?
66
+ end
67
+
68
+ def download
69
+ @order = Piggybak::Order.find(params[:id])
70
+
71
+ if can?(:download, @order)
72
+ render :layout => false
73
+ else
74
+ render "no_access"
75
+ end
68
76
  end
69
77
 
70
78
  def email
71
79
  order = Order.find(params[:id])
72
- Piggybak::Notifier.order_notification(order)
73
- flash[:notice] = "Email notification sent."
80
+
81
+ if can?(:email, order)
82
+ Piggybak::Notifier.order_notification(order)
83
+ flash[:notice] = "Email notification sent."
84
+ end
74
85
 
75
86
  redirect_to rails_admin.edit_path('Piggybak::Order', order.id)
76
87
  end
77
88
 
89
+ # AJAX Actions from checkout
78
90
  def shipping
79
91
  cart = Piggybak::Cart.new(request.cookies["cart"])
80
92
  cart.extra_data = params
@@ -88,5 +100,14 @@ module Piggybak
88
100
  total_tax = Piggybak::TaxMethod.calculate_tax(cart)
89
101
  render :json => { :tax => total_tax }
90
102
  end
103
+
104
+ def geodata
105
+ countries = ::Piggybak::Country.find(:all, :include => :states)
106
+ data = countries.inject({}) do |h, country|
107
+ h["country_#{country.id}"] = country.states
108
+ h
109
+ end
110
+ render :json => { :countries => data }
111
+ end
91
112
  end
92
113
  end
@@ -1,4 +1,4 @@
1
- module ApplicationHelper
1
+ module PiggybakHelper
2
2
  def cart_form(object)
3
3
  render "piggybak/cart/form", :object => object
4
4
  end
@@ -9,4 +9,9 @@ module ApplicationHelper
9
9
  link_to "#{pluralize(nitems, 'item')}: #{number_to_currency(cart.total)}", piggybak.cart_url
10
10
  end
11
11
  end
12
+ def orders_link(text)
13
+ if current_user
14
+ link_to text, piggybak.orders_list_url
15
+ end
16
+ end
12
17
  end
@@ -1,13 +1,25 @@
1
1
  module Piggybak
2
2
  class Address < ActiveRecord::Base
3
3
  belongs_to :state
4
+ belongs_to :country
4
5
 
5
6
  validates_presence_of :firstname
6
7
  validates_presence_of :lastname
7
8
  validates_presence_of :address1
8
9
  validates_presence_of :city
9
10
  validates_presence_of :state_id
11
+ validates_presence_of :country_id
10
12
  validates_presence_of :zip
13
+
14
+ after_initialize :set_default_country
15
+
16
+ def set_default_country
17
+ self.country ||= Address.DEFAULT_COUNTRY
18
+ end
19
+
20
+ def self.DEFAULT_COUNTRY
21
+ Country.find_by_abbr("US")
22
+ end
11
23
 
12
24
  def admin_label
13
25
  address = "#{self.firstname} #{self.lastname}<br />"
@@ -15,7 +27,8 @@ module Piggybak
15
27
  if self.address2 && self.address2 != ''
16
28
  address += "#{self.address2}<br />"
17
29
  end
18
- address += "#{self.city}, #{self.state.abbr} #{self.zip}"
30
+ address += "#{self.city}, #{self.state ? self.state.name : self.state_id} #{self.zip}<br />"
31
+ address += "#{self.country.name}"
19
32
  address
20
33
  end
21
34
  alias :display :admin_label
@@ -10,9 +10,12 @@ module Piggybak
10
10
  self.errors = []
11
11
  cookie ||= ''
12
12
  cookie.split(';').each do |item|
13
- self.items << { :product => Piggybak::Product.find(item.split(':')[0]), :quantity => (item.split(':')[1]).to_i }
13
+ item_variant = Piggybak::Variant.find_by_id(item.split(':')[0])
14
+ if item_variant.present?
15
+ self.items << { :variant => item_variant, :quantity => (item.split(':')[1]).to_i }
16
+ end
14
17
  end
15
- self.total = self.items.sum { |item| item[:quantity]*item[:product].price }
18
+ self.total = self.items.sum { |item| item[:quantity]*item[:variant].price }
16
19
  end
17
20
 
18
21
  def self.to_hash(cookie)
@@ -33,14 +36,14 @@ module Piggybak
33
36
 
34
37
  def self.add(cookie, params)
35
38
  cart = to_hash(cookie)
36
- cart["#{params[:product_id]}"] ||= 0
37
- cart["#{params[:product_id]}"] += params[:quantity].to_i
39
+ cart["#{params[:variant_id]}"] ||= 0
40
+ cart["#{params[:variant_id]}"] += params[:quantity].to_i
38
41
  to_string(cart)
39
42
  end
40
43
 
41
- def self.remove(cookie, product_id)
44
+ def self.remove(cookie, variant_id)
42
45
  cart = to_hash(cookie)
43
- cart[product_id] = 0
46
+ cart[variant_id] = 0
44
47
  to_string(cart)
45
48
  end
46
49
 
@@ -53,7 +56,7 @@ module Piggybak
53
56
  def to_cookie
54
57
  cookie = ''
55
58
  self.items.each do |item|
56
- cookie += "#{item[:product].id.to_s}:#{item[:quantity].to_s};" if item[:quantity].to_i > 0
59
+ cookie += "#{item[:variant].id.to_s}:#{item[:quantity].to_s};" if item[:quantity].to_i > 0
57
60
  end
58
61
  cookie
59
62
  end
@@ -62,16 +65,20 @@ module Piggybak
62
65
  self.errors = []
63
66
  new_items = []
64
67
  self.items.each do |item|
65
- if item[:product].unlimited_inventory || item[:product].quantity >= item[:quantity]
68
+ if !item[:variant].active
69
+ self.errors << ["Sorry, #{item[:variant].description} is no longer for sale"]
70
+ elsif item[:variant].unlimited_inventory || item[:variant].quantity >= item[:quantity]
66
71
  new_items << item
72
+ elsif item[:variant].quantity == 0
73
+ self.errors << ["Sorry, #{item[:variant].description} is no longer available"]
67
74
  else
68
- self.errors << ["Adjusting quantity for #{item[:product].description}"]
69
- item[:quantity] = item[:product].quantity
70
- new_items << item
75
+ self.errors << ["Sorry, only #{item[:variant].quantity} available for #{item[:variant].description}"]
76
+ item[:quantity] = item[:variant].quantity
77
+ new_items << item if item[:quantity] > 0
71
78
  end
72
79
  end
73
80
  self.items = new_items
74
- self.total = self.items.sum { |item| item[:quantity]*item[:product].price }
81
+ self.total = self.items.sum { |item| item[:quantity]*item[:variant].price }
75
82
  end
76
83
  end
77
84
  end
@@ -0,0 +1,5 @@
1
+ module Piggybak
2
+ class Country < ActiveRecord::Base
3
+ has_many :states
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ module Piggybak
2
+ class Credit < ActiveRecord::Base
3
+ belongs_to :order
4
+ belongs_to :source, :polymorphic => true
5
+
6
+ validates_presence_of :total
7
+ end
8
+ end