piggybak_coupons 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +40 -0
- data/app/assets/javascripts/piggybak_coupons/piggybak_coupons.js +58 -0
- data/app/assets/stylesheets/piggybak_coupons/application.css +13 -0
- data/app/controllers/piggybak_coupons/coupon_controller.rb +14 -0
- data/app/models/piggybak_coupons/coupon.rb +85 -0
- data/app/models/piggybak_coupons/coupon_application.rb +24 -0
- data/app/views/layouts/piggybak_coupons/application.html.erb +14 -0
- data/app/views/piggybak_coupons/_apply_coupon.html.erb +11 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20121012220537_piggybak_coupons_setup.rb +21 -0
- data/lib/piggybak_coupons/engine.rb +72 -0
- data/lib/piggybak_coupons/line_item_decorator.rb +21 -0
- data/lib/piggybak_coupons/version.rb +3 -0
- data/lib/piggybak_coupons.rb +4 -0
- data/lib/tasks/piggybak_coupons_tasks.rake +4 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/piggybak_coupons_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +146 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
= PiggybakCoupons
|
2
|
+
|
3
|
+
Coupon support for Piggybak. Includes types %, $, and freeshipping. Coupons have a specified amount (tied to % and $ use), a minimum cart total, expiration date, and number of uses.
|
4
|
+
|
5
|
+
CanCan access must be updated to allow admin access to view coupons.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'PiggybakCoupons'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
Bundler::GemHelper.install_tasks
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
|
32
|
+
Rake::TestTask.new(:test) do |t|
|
33
|
+
t.libs << 'lib'
|
34
|
+
t.libs << 'test'
|
35
|
+
t.pattern = 'test/**/*_test.rb'
|
36
|
+
t.verbose = false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
@@ -0,0 +1,58 @@
|
|
1
|
+
$(function() {
|
2
|
+
$('#shipping select').change(function() {
|
3
|
+
piggybak_coupons.apply_coupon(false);
|
4
|
+
return false;
|
5
|
+
});
|
6
|
+
$('#apply_coupon').click(function() {
|
7
|
+
piggybak_coupons.apply_coupon(false);
|
8
|
+
return false;
|
9
|
+
});
|
10
|
+
$('#submit input').click(function() {
|
11
|
+
piggybak_coupons.apply_coupon(true);
|
12
|
+
return false;
|
13
|
+
});
|
14
|
+
setTimeout(function() {
|
15
|
+
if($('#coupon_code').val() != '') {
|
16
|
+
piggybak_coupons.apply_coupon(false);
|
17
|
+
}
|
18
|
+
}, 500);
|
19
|
+
});
|
20
|
+
|
21
|
+
var piggybak_coupons = {
|
22
|
+
apply_coupon: function(on_submit) {
|
23
|
+
$('#coupon input[type=hidden]').remove();
|
24
|
+
$('#coupon_response').hide();
|
25
|
+
$.ajax({
|
26
|
+
url: coupon_lookup,
|
27
|
+
cached: false,
|
28
|
+
data: {
|
29
|
+
code: $('#coupon_code').val(),
|
30
|
+
shipcost: $('#shipping_total').html().replace(/^\$/, '')
|
31
|
+
},
|
32
|
+
dataType: "JSON",
|
33
|
+
success: function(data) {
|
34
|
+
if(data.valid_coupon) {
|
35
|
+
var el1 = $('<input>').attr('type', 'hidden').attr('name', 'piggybak_order[line_items_attributes][2][line_item_type]').val('coupon_application');
|
36
|
+
var el2 = $('<input>').attr('type', 'hidden').attr('name', 'piggybak_order[line_items_attributes][2][coupon_application_attributes][code]').val($('#coupon_code').val());
|
37
|
+
$('#coupon').append(el1);
|
38
|
+
$('#coupon').append(el2);
|
39
|
+
$('#coupon_response').html('Coupon successfully applied to order.');
|
40
|
+
$('#coupon_application_total').html('-$' + (-1*parseFloat(data.amount)).toFixed(2));
|
41
|
+
$('#coupon_application_row').show();
|
42
|
+
piggybak.update_totals();
|
43
|
+
} else {
|
44
|
+
$('#coupon_response').html(data.message).show();
|
45
|
+
$('#coupon_application_total').html('$0.00');
|
46
|
+
$('#coupon_application_row').hide();
|
47
|
+
piggybak.update_totals();
|
48
|
+
}
|
49
|
+
if(on_submit) {
|
50
|
+
$('#new_piggybak_order').submit();
|
51
|
+
}
|
52
|
+
},
|
53
|
+
error: function() {
|
54
|
+
//do nothing right now
|
55
|
+
}
|
56
|
+
});
|
57
|
+
}
|
58
|
+
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module PiggybakCoupons
|
2
|
+
class CouponController < ApplicationController
|
3
|
+
def apply
|
4
|
+
cart = Piggybak::Cart.new(request.cookies["cart"])
|
5
|
+
valid_coupon = ::PiggybakCoupons::Coupon.valid_coupon(params[:code], cart, true)
|
6
|
+
if valid_coupon.is_a?(::PiggybakCoupons::Coupon)
|
7
|
+
amount = ::PiggybakCoupons::Coupon.apply_discount(params[:code], cart, params[:shipcost])
|
8
|
+
render :json => { :valid_coupon => true, :amount => amount }
|
9
|
+
else
|
10
|
+
render :json => { :valid_coupon => false, :message => valid_coupon }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module PiggybakCoupons
|
2
|
+
class Coupon < ActiveRecord::Base
|
3
|
+
self.table_name = 'coupons'
|
4
|
+
|
5
|
+
has_many :coupon_applications
|
6
|
+
|
7
|
+
attr_accessor :coupon_type, :application_detail
|
8
|
+
attr_accessible :code, :amount, :discount_type, :min_cart_total, :expiration_date, :allowed_applications
|
9
|
+
|
10
|
+
validates_presence_of :code, :amount, :discount_type, :min_cart_total, :expiration_date, :allowed_applications
|
11
|
+
validates_uniqueness_of :code
|
12
|
+
validates_numericality_of :amount, :greater_than_or_equal_to => 0
|
13
|
+
validates_numericality_of :min_cart_total, :greater_than_or_equal_to => 0
|
14
|
+
validates_numericality_of :allowed_applications, :greater_than_or_equal_to => 0
|
15
|
+
validate :validate_dollar_discount
|
16
|
+
|
17
|
+
def validate_dollar_discount
|
18
|
+
if self.discount_type == "$" && self.amount > self.min_cart_total
|
19
|
+
self.errors.add(:min_cart_total, "Minimum cart total must be greater than amount for dollar discount.")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def coupon_type
|
24
|
+
if self.discount_type == "ship"
|
25
|
+
return "free shipping"
|
26
|
+
elsif self.discount_type == "%"
|
27
|
+
return "#{self.amount}#{self.discount_type}"
|
28
|
+
elsif self.discount_type == "$"
|
29
|
+
return "#{self.discount_type}#{sprintf("%.2f", self.amount)}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def discount_type_enum
|
34
|
+
[['Percent', '%'], ['Dollar', '$'], ['Free Shipping', 'ship']]
|
35
|
+
end
|
36
|
+
|
37
|
+
def application_detail
|
38
|
+
"#{self.coupon_applications.size} of #{self.allowed_applications} allowed uses applied"
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.valid_coupon(code, object, already_applied)
|
42
|
+
# First check
|
43
|
+
coupon = Coupon.find_by_code(code)
|
44
|
+
return "Invalid coupon code." if coupon.nil?
|
45
|
+
|
46
|
+
# Expiration date check
|
47
|
+
return "Expired coupon." if coupon.expiration_date < Date.today
|
48
|
+
|
49
|
+
# Min cart total check
|
50
|
+
return "Order does not meet minimum total for coupon." if object.subtotal < coupon.min_cart_total.to_f
|
51
|
+
|
52
|
+
# Allowed applications check
|
53
|
+
return "Coupon has already been used #{coupon.allowed_applications} times." if !already_applied && (coupon.coupon_applications.size >= coupon.allowed_applications)
|
54
|
+
|
55
|
+
if object.is_a?(Piggybak::Order) && coupon.discount_type == "ship"
|
56
|
+
ship_line_item = object.line_items.detect { |li| li.line_item_type == "shipment" }
|
57
|
+
return "No shipping on this order." if !ship_line_item
|
58
|
+
end
|
59
|
+
coupon
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.apply_discount(code, object, shipcost = 0.0)
|
63
|
+
coupon = Coupon.find_by_code(code)
|
64
|
+
return 0 if coupon.nil?
|
65
|
+
|
66
|
+
# $ or % discount_type discount
|
67
|
+
if coupon.discount_type == "$"
|
68
|
+
return -1*coupon.amount
|
69
|
+
elsif coupon.discount_type == "%"
|
70
|
+
return (-1.to_f*(coupon.amount/100)*object.subtotal).to_c
|
71
|
+
elsif coupon.discount_type == "ship"
|
72
|
+
if object.is_a?(Piggybak::Order)
|
73
|
+
ship_line_item = object.line_items.detect { |li| li.line_item_type == "shipment" }
|
74
|
+
if ship_line_item
|
75
|
+
return -1*ship_line_item.price
|
76
|
+
else
|
77
|
+
return 0.00
|
78
|
+
end
|
79
|
+
elsif object.is_a?(Piggybak::Cart)
|
80
|
+
return -1*shipcost.to_f
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module PiggybakCoupons
|
2
|
+
class CouponApplication < ActiveRecord::Base
|
3
|
+
# TODO: figure out how to avoid this
|
4
|
+
set_table_name 'coupon_applications'
|
5
|
+
|
6
|
+
belongs_to :coupon
|
7
|
+
belongs_to :line_item, :class_name => "::Piggybak::LineItem", :dependent => :destroy
|
8
|
+
|
9
|
+
attr_accessor :code, :order
|
10
|
+
attr_accessible :line_item_id, :coupon_id, :code
|
11
|
+
|
12
|
+
validate :validate_coupon
|
13
|
+
def validate_coupon
|
14
|
+
if self.new_record?
|
15
|
+
valid_coupon = Coupon.valid_coupon(self.code, self.order, false)
|
16
|
+
if valid_coupon.is_a?(::PiggybakCoupons::Coupon)
|
17
|
+
self.coupon_id = valid_coupon.id
|
18
|
+
else
|
19
|
+
self.errors.add(:code, valid_coupon)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>PiggybakCoupons</title>
|
5
|
+
<%= stylesheet_link_tag "piggybak_coupons/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "piggybak_coupons/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%= javascript_include_tag "piggybak_coupons/piggybak_coupons" %>
|
2
|
+
|
3
|
+
<div id="piggybak_coupon_apply_box">
|
4
|
+
<div id="coupon_response" style="display:none;"></div>
|
5
|
+
<input type="text" name="piggybak_coupon_code" id="coupon_code" value="<%= params.has_key?(:piggybak_coupon_code) ? params[:piggybak_coupon_code] : '' %>" />
|
6
|
+
<input type="submit" value="Apply Coupon" id="apply_coupon" />
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<script type="text/javascript">
|
10
|
+
var coupon_lookup = "<%= piggybak_coupons.apply_coupon_url %>";
|
11
|
+
</script>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class PiggybakCouponsSetup < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
create_table :coupons do |t|
|
4
|
+
t.string :code, :null => false
|
5
|
+
t.decimal :amount, :null => false
|
6
|
+
t.string :discount_type, :null => false
|
7
|
+
t.decimal :min_cart_total, :null => false
|
8
|
+
t.date :expiration_date
|
9
|
+
t.integer :allowed_applications
|
10
|
+
end
|
11
|
+
create_table :coupon_applications do |t|
|
12
|
+
t.references :line_item
|
13
|
+
t.references :coupon
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def down
|
18
|
+
drop_table :coupons
|
19
|
+
drop_table :coupon_applications
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'piggybak_coupons/line_item_decorator'
|
2
|
+
|
3
|
+
module PiggybakCoupons
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace PiggybakCoupons
|
6
|
+
|
7
|
+
config.to_prepare do
|
8
|
+
Piggybak::LineItem.send(:include, ::PiggybakCoupons::LineItemDecorator)
|
9
|
+
end
|
10
|
+
|
11
|
+
config.before_initialize do
|
12
|
+
Piggybak.config do |config|
|
13
|
+
config.line_item_types[:coupon_application] = { :visible => true,
|
14
|
+
:nested_attrs => true,
|
15
|
+
:fields => ["coupon_application"],
|
16
|
+
:allow_destroy => true,
|
17
|
+
:class_name => "::PiggybakCoupons::CouponApplication",
|
18
|
+
:display_in_cart => "Discount"
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "piggybak_coupons.precompile_hook" do |app|
|
24
|
+
app.config.assets.precompile += ['piggybak_coupons/piggybak_coupons.js']
|
25
|
+
end
|
26
|
+
|
27
|
+
initializer "piggybak_coupons.rails_admin_config" do |app|
|
28
|
+
RailsAdmin.config do |config|
|
29
|
+
config.model PiggybakCoupons::Coupon do
|
30
|
+
navigation_label "Orders"
|
31
|
+
label "Coupon"
|
32
|
+
|
33
|
+
list do
|
34
|
+
field :code
|
35
|
+
field :coupon_type
|
36
|
+
field :min_cart_total do
|
37
|
+
formatted_value do
|
38
|
+
"$#{sprintf("%.2f", value)}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
field :expiration_date
|
42
|
+
field :application_detail
|
43
|
+
end
|
44
|
+
edit do
|
45
|
+
field :code
|
46
|
+
field :amount
|
47
|
+
field :discount_type, :enum
|
48
|
+
field :min_cart_total
|
49
|
+
field :expiration_date
|
50
|
+
field :allowed_applications
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
config.model PiggybakCoupons::CouponApplication do
|
55
|
+
label "Coupon"
|
56
|
+
visible false
|
57
|
+
|
58
|
+
edit do
|
59
|
+
field :code do
|
60
|
+
read_only do
|
61
|
+
!bindings[:object].new_record?
|
62
|
+
end
|
63
|
+
pretty_value do
|
64
|
+
bindings[:object].coupon ? bindings[:object].coupon.code : ""
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PiggybakCoupons
|
2
|
+
module LineItemDecorator
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def preprocess_coupon_application
|
6
|
+
self.description = "Coupon"
|
7
|
+
self.coupon_application.order = self.order
|
8
|
+
if !self.new_record?
|
9
|
+
valid_coupon = ::PiggybakCoupons::Coupon.valid_coupon(self.coupon_application.coupon.code, self.order, true)
|
10
|
+
if !valid_coupon.is_a?(::PiggybakCoupons::Coupon)
|
11
|
+
self.mark_for_destruction
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def postprocess_coupon_application
|
17
|
+
self.price = ::PiggybakCoupons::Coupon.apply_discount(self.coupon_application.coupon.code, self.order)
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|