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.
- 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
|