piggybak 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +5 -0
- data/README.md +117 -0
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/app/assets/javascripts/piggybak.js +62 -2
- data/app/controllers/piggybak/orders_controller.rb +26 -5
- data/{lib/application_helper.rb → app/helpers/piggybak_helper.rb} +6 -1
- data/app/models/piggybak/address.rb +14 -1
- data/app/models/piggybak/cart.rb +19 -12
- data/app/models/piggybak/country.rb +5 -0
- data/app/models/piggybak/credit.rb +8 -0
- data/app/models/piggybak/line_item.rb +13 -6
- data/app/models/piggybak/order.rb +12 -6
- data/app/models/piggybak/payment.rb +5 -3
- data/app/models/piggybak/payment_calculator/authorize_net.rb +4 -0
- data/app/models/piggybak/payment_calculator/fake.rb +4 -0
- data/app/models/piggybak/payment_method.rb +8 -3
- data/app/models/piggybak/shipping_calculator/free.rb +13 -0
- data/app/models/piggybak/shipping_calculator/pickup.rb +1 -1
- data/app/models/piggybak/shipping_method.rb +9 -7
- data/app/models/piggybak/state.rb +1 -0
- data/app/models/piggybak/tax_calculator/{flat_rate.rb → percent.rb} +1 -1
- data/app/models/piggybak/tax_method.rb +8 -2
- data/app/models/piggybak/{product.rb → variant.rb} +5 -4
- data/app/views/piggybak/cart/_form.html.erb +7 -7
- data/app/views/piggybak/cart/_items.html.erb +9 -9
- data/app/views/piggybak/cart/show.html.erb +2 -0
- data/app/views/piggybak/notifier/order_notification.text.erb +1 -1
- data/app/views/piggybak/orders/_address_form.html.erb +18 -14
- data/app/views/piggybak/orders/_details.html.erb +48 -0
- data/app/views/piggybak/orders/download.text.erb +19 -0
- data/app/views/piggybak/orders/list.html.erb +12 -12
- data/app/views/piggybak/orders/no_access.text.erb +1 -0
- data/app/views/piggybak/orders/receipt.html.erb +1 -49
- data/app/views/piggybak/orders/show.html.erb +36 -31
- data/app/views/rails_admin/main/_actions.html.erb +9 -2
- data/config/routes.rb +12 -3
- data/db/migrate/20111227150106_create_orders.rb +4 -3
- data/db/migrate/20111227150322_create_addresses.rb +2 -1
- data/db/migrate/20111227150432_create_line_items.rb +2 -2
- data/db/migrate/{20111227213558_create_products.rb → 20111227213558_create_variants.rb} +3 -3
- data/db/migrate/20111228231829_create_payments.rb +3 -1
- data/db/migrate/20111228231838_create_shipments.rb +1 -1
- data/db/migrate/20120102162414_create_countries.rb +10 -0
- data/db/migrate/20120102162415_create_states.rb +1 -0
- data/db/migrate/20120104020930_populate_countries_and_states.rb +18 -0
- data/db/migrate/20120106010412_create_credits.rb +14 -0
- data/lib/{acts_as_product → acts_as_variant}/base.rb +6 -6
- data/lib/piggybak.rb +59 -23
- data/piggybak.gemspec +26 -11
- metadata +64 -62
- data/README.rdoc +0 -19
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -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
|
data/README.md
ADDED
@@ -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
|
+
0.2.0
|
@@ -1,13 +1,54 @@
|
|
1
1
|
var tax_total = 0;
|
2
|
+
var geodata;
|
2
3
|
|
3
4
|
$(function() {
|
4
|
-
piggybak.
|
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 =
|
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
|
-
|
67
|
-
|
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
|
-
|
73
|
-
|
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
|
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.
|
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
|
data/app/models/piggybak/cart.rb
CHANGED
@@ -10,9 +10,12 @@ module Piggybak
|
|
10
10
|
self.errors = []
|
11
11
|
cookie ||= ''
|
12
12
|
cookie.split(';').each do |item|
|
13
|
-
|
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[:
|
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[:
|
37
|
-
cart["#{params[:
|
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,
|
44
|
+
def self.remove(cookie, variant_id)
|
42
45
|
cart = to_hash(cookie)
|
43
|
-
cart[
|
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[:
|
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[:
|
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 << ["
|
69
|
-
item[:quantity] = item[:
|
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[:
|
81
|
+
self.total = self.items.sum { |item| item[:quantity]*item[:variant].price }
|
75
82
|
end
|
76
83
|
end
|
77
84
|
end
|