piggybak 0.1.1 → 0.2.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 (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