diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..563f1b1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+db/schema.rb
+log
+tmp
+**DS_Store
+config/google_checkout.yml
+config/paypal.yml
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..ff9db4f
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,22 @@
+source 'http://rubygems.org'
+
+gem 'rails', '3.2.2'
+gem 'foreigner'
+gem "google4r-checkout", :git => 'https://github.com/nbudin/google4r-checkout.git'
+gem "money", "~> 4.0.1"
+gem 'mysql2', '~> 0.3.0'
+gem 'uuidtools'
+gem "xml-simple", "~> 1.1.1"
+gem 'will_paginate'
+gem 'dynamic_form'
+gem "exception_notification", "~> 2.5.2", :require => 'exception_notifier'
+gem 'jquery-rails', '>= 1.0.12'
+
+#gem 'heroku'
+#gem 'pg'
+
+group :assets do
+ gem 'sass-rails'
+ gem 'coffee-rails'
+ gem 'uglifier'
+end
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..cfa8326
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,137 @@
+GIT
+ remote: https://github.com/nbudin/google4r-checkout.git
+ revision: 46d0823fd220480d971bb95f75cbc0e6ed238d07
+ specs:
+ google4r-checkout (1.1.1)
+ money (>= 2.3.0)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ actionmailer (3.2.2)
+ actionpack (= 3.2.2)
+ mail (~> 2.4.0)
+ actionpack (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ builder (~> 3.0.0)
+ erubis (~> 2.7.0)
+ journey (~> 1.0.1)
+ rack (~> 1.4.0)
+ rack-cache (~> 1.1)
+ rack-test (~> 0.6.1)
+ sprockets (~> 2.1.2)
+ activemodel (3.2.2)
+ activesupport (= 3.2.2)
+ builder (~> 3.0.0)
+ activerecord (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ arel (~> 3.0.2)
+ tzinfo (~> 0.3.29)
+ activeresource (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ activesupport (3.2.2)
+ i18n (~> 0.6)
+ multi_json (~> 1.0)
+ arel (3.0.2)
+ builder (3.0.0)
+ coffee-rails (3.2.2)
+ coffee-script (>= 2.2.0)
+ railties (~> 3.2.0)
+ coffee-script (2.2.0)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.2.0)
+ dynamic_form (1.1.4)
+ erubis (2.7.0)
+ exception_notification (2.5.2)
+ actionmailer (>= 3.0.4)
+ execjs (1.3.0)
+ multi_json (~> 1.0)
+ foreigner (1.1.5)
+ activerecord (>= 3.0.0)
+ hike (1.2.1)
+ i18n (0.6.0)
+ journey (1.0.3)
+ jquery-rails (2.0.1)
+ railties (>= 3.2.0, < 5.0)
+ thor (~> 0.14)
+ json (1.6.6)
+ mail (2.4.4)
+ i18n (>= 0.4.0)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.18)
+ money (4.0.2)
+ i18n (~> 0.4)
+ json
+ multi_json (1.2.0)
+ mysql2 (0.3.11)
+ polyglot (0.3.3)
+ rack (1.4.1)
+ rack-cache (1.2)
+ rack (>= 0.4)
+ rack-ssl (1.3.2)
+ rack
+ rack-test (0.6.1)
+ rack (>= 1.0)
+ rails (3.2.2)
+ actionmailer (= 3.2.2)
+ actionpack (= 3.2.2)
+ activerecord (= 3.2.2)
+ activeresource (= 3.2.2)
+ activesupport (= 3.2.2)
+ bundler (~> 1.0)
+ railties (= 3.2.2)
+ railties (3.2.2)
+ actionpack (= 3.2.2)
+ activesupport (= 3.2.2)
+ rack-ssl (~> 1.3.2)
+ rake (>= 0.8.7)
+ rdoc (~> 3.4)
+ thor (~> 0.14.6)
+ rake (0.9.2.2)
+ rdoc (3.12)
+ json (~> 1.4)
+ sass (3.1.15)
+ sass-rails (3.2.5)
+ railties (~> 3.2.0)
+ sass (>= 3.1.10)
+ tilt (~> 1.3)
+ sprockets (2.1.2)
+ hike (~> 1.2)
+ rack (~> 1.0)
+ tilt (~> 1.1, != 1.3.0)
+ thor (0.14.6)
+ tilt (1.3.3)
+ treetop (1.4.10)
+ polyglot
+ polyglot (>= 0.3.1)
+ tzinfo (0.3.32)
+ uglifier (1.2.4)
+ execjs (>= 0.3.0)
+ multi_json (>= 1.0.2)
+ uuidtools (2.1.2)
+ will_paginate (3.0.3)
+ xml-simple (1.1.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ coffee-rails
+ dynamic_form
+ exception_notification (~> 2.5.2)
+ foreigner
+ google4r-checkout!
+ jquery-rails (>= 1.0.12)
+ money (~> 4.0.1)
+ mysql2 (~> 0.3.0)
+ rails (= 3.2.2)
+ sass-rails
+ uglifier
+ uuidtools
+ will_paginate
+ xml-simple (~> 1.1.1)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b6bf8f0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+Copyright 2007, Potion Factory LLC
+
+Unless noted otherwise, the files of this project are licensed under a
+Creative Commons Attribution-Share Alike License.
+
+http://creativecommons.org/licenses/by-sa/3.0/
+
+###
+
+Some software components bundled with this software are licensed to
+Potion Factory LLC under their own terms. Please see the following
+files for details:
+
+vendor/plugins/google4r/LICENSE
+vendor/plugins/paypal/LPGL-LICENSE
+
+The printer icon in public/images/printer.png was created by Mark James at FamFamFam.com
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..979480a
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,132 @@
+Welcome to Potion Store
+-----------------------
+
+Features:
+
+- PayPal Website Payments Pro support
+- PayPal Express Checkout support
+- Google Checkout support
+- Administration interface with some simple sales charts
+- Coupons
+- Send lost license page (http://mycompany.com/store/lost_license)
+- Google Analytics e-commerce transaction tracking support for PayPal and credit card orders
+
+
+Dependencies
+------------
+
+- Rails 3.0 or higher.
+- PostgreSQL or MySQL
+
+
+Installation
+------------
+
+This is a brief outline of the steps required to get the development environment of Potion Store up
+and running on your local machine.
+
+- Install gems via Bundler
+ - Run ```bundle install```
+
+- Edit the following config files to suit your needs
+
+ - config/store.yml
+ - config/paypal.yml
+ - config/google_checkout.yml
+
+- Create config/google_checkout.yml. Modify it with your credentials.
+ ```
+ # Settings for Google Checkout
+
+ # Get these by logging into Google Checkout's merchant site and the sandbox equivalent
+
+ development:
+ gcheckout_merchant_id: "XXXXXXXXXXXXXXX"
+ gcheckout_merchant_key: "XXXXXXXXXXXXXXXXXXXXXX"
+
+ test:
+ gcheckout_merchant_id: "XXXXXXXXXXXXXXX"
+ gcheckout_merchant_key: "XXXXXXXXXXXXXXXXXXXXXX"
+
+ production:
+ gcheckout_merchant_id: "XXXXXXXXXXXXXXX"
+ gcheckout_merchant_key: "XXXXXXXXXXXXXXXXXXXXXX"
+ ```
+
+- Create config/paypal.yml. Modify it with your credentials.
+ ```
+ # PayPal API Access Setup
+ #
+ # Instructions:
+ #
+ # 1. Go to https://developer.paypal.com/.
+ # 2. Create an account if you don't have one.
+ # 3. Click the "Test Accounts" on the left sidebar.
+ # 4. Create a Business Test Account if you don't have one.
+ # 5. Select the Business Test Account and click the "Enter Sandbox Test Site" button.
+ # 6. Once you log in, click "Profile."
+ # 7. Click on "API Access."
+ # 8. Click the "Request API Credentials" link.
+ # 9. Leave the default selection on "Request API signature" and click the "Agree and Submit" button.
+ # 10. Fill in api_username and api_password and api_signature with the given information.
+
+ # Development settings
+ development:
+ pi_username: "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
+ api_password: "XXXXXXXXXXXXXXXX"
+ api_signature: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+ wiredump: true # Turn on logging of communications with PayPal during development
+
+ # Follow steps 6 to 10 but with your real PayPal account.
+ # NOTE: Your PayPal account must have Website Payments Pro already for this to work.
+
+ # Live settings
+ production:
+ api_username: "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
+ api_password: "XXXXXXXXXXXXXXXX"
+ api_signature: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+ # Don't turn this on in production mode unless you absolutely have to. It'll log people's credit card information
+ wiredump: false
+
+ # Common setting
+ # NOTE: This file is already provided for you. You don't need to download it again.
+ ca_file: "certs/api_cert_chain.crt"
+ ```
+- Set session store secret
+ Edit config/environment.rb and modify the config.action_controller.session setting
+
+- Setup database
+ - Install Postgresql or MySQL if you haven't
+ - Create the store_development database.
+ Make sure to set the encoding of the database to UTF8.
+ I recommend pgAdmin for Postgresql newcomers.
+ - Edit config/database.yml
+ - run "rake db:migrate" to create the database schema
+
+- Run ```rails s``` and test through
+ and
+
+
+- Replace the default license key generator in lib/licensekey.rb with your own
+
+- If you are setting up Google Checkout, log into your Google Checkout account (sandbox or live), go
+ to Settings->Integration and put in your URL that corresponds to the following:
+
+ https://secure.potionfactory.com/store/notification/gcheckout
+
+ That is the URL that Google uses to make callbacks. If you don't set this up, your customers will
+ not get their orders delivered by email.
+
+
+Debugging
+---------
+
+1. ```gem install ruby-debug```
+2. Put 'debugger' where you want to break in your source code
+3. Start the app with ```rails s --debugger``` to enable breakpoints
+
+
+Final Notes
+-----------
+
+I'd appreciate it if you kept the "Powered by Potion Store" link in the footer. It'll help more developers find the project.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..3edb70c
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,7 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require File.expand_path('../config/application', __FILE__)
+require 'rake'
+
+Potionstore::Application.load_tasks
diff --git a/app/assets/images/bg.png b/app/assets/images/bg.png
new file mode 100644
index 0000000..e69de29
diff --git a/app/assets/images/store/amex.gif b/app/assets/images/store/amex.gif
new file mode 100644
index 0000000..f31a0ba
Binary files /dev/null and b/app/assets/images/store/amex.gif differ
diff --git a/app/assets/images/store/application_icon.png b/app/assets/images/store/application_icon.png
new file mode 100644
index 0000000..3cd950c
Binary files /dev/null and b/app/assets/images/store/application_icon.png differ
diff --git a/app/assets/images/store/cvv.png b/app/assets/images/store/cvv.png
new file mode 100644
index 0000000..33ce051
Binary files /dev/null and b/app/assets/images/store/cvv.png differ
diff --git a/app/assets/images/store/discover.gif b/app/assets/images/store/discover.gif
new file mode 100644
index 0000000..26f02fb
Binary files /dev/null and b/app/assets/images/store/discover.gif differ
diff --git a/app/assets/images/store/gcheckout.gif b/app/assets/images/store/gcheckout.gif
new file mode 100644
index 0000000..ffe58c1
Binary files /dev/null and b/app/assets/images/store/gcheckout.gif differ
diff --git a/app/assets/images/store/mc.gif b/app/assets/images/store/mc.gif
new file mode 100644
index 0000000..97f0982
Binary files /dev/null and b/app/assets/images/store/mc.gif differ
diff --git a/app/assets/images/store/missing_field.png b/app/assets/images/store/missing_field.png
new file mode 100644
index 0000000..ae54ebf
Binary files /dev/null and b/app/assets/images/store/missing_field.png differ
diff --git a/app/assets/images/store/paypal.gif b/app/assets/images/store/paypal.gif
new file mode 100644
index 0000000..25333b1
Binary files /dev/null and b/app/assets/images/store/paypal.gif differ
diff --git a/app/assets/images/store/printer.png b/app/assets/images/store/printer.png
new file mode 100644
index 0000000..a350d18
Binary files /dev/null and b/app/assets/images/store/printer.png differ
diff --git a/app/assets/images/store/rounded_tr.png b/app/assets/images/store/rounded_tr.png
new file mode 100644
index 0000000..175f478
Binary files /dev/null and b/app/assets/images/store/rounded_tr.png differ
diff --git a/app/assets/images/store/visa.gif b/app/assets/images/store/visa.gif
new file mode 100644
index 0000000..1250cec
Binary files /dev/null and b/app/assets/images/store/visa.gif differ
diff --git a/app/assets/javascripts/admin/coupons.js b/app/assets/javascripts/admin/coupons.js
new file mode 100644
index 0000000..dee720f
--- /dev/null
+++ b/app/assets/javascripts/admin/coupons.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000..4acc1cc
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -0,0 +1,4 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.validate.min
+//= require store
\ No newline at end of file
diff --git a/app/assets/javascripts/jquery.validate.min.js b/app/assets/javascripts/jquery.validate.min.js
new file mode 100644
index 0000000..edd6452
--- /dev/null
+++ b/app/assets/javascripts/jquery.validate.min.js
@@ -0,0 +1,51 @@
+/**
+ * jQuery Validation Plugin 1.9.0
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-validation/
+ * http://docs.jquery.com/Plugins/Validation
+ *
+ * Copyright (c) 2006 - 2011 Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+(function(c){c.extend(c.fn,{validate:function(a){if(this.length){var b=c.data(this[0],"validator");if(b)return b;this.attr("novalidate","novalidate");b=new c.validator(a,this[0]);c.data(this[0],"validator",b);if(b.settings.onsubmit){a=this.find("input, button");a.filter(".cancel").click(function(){b.cancelSubmit=true});b.settings.submitHandler&&a.filter(":submit").click(function(){b.submitButton=this});this.submit(function(d){function e(){if(b.settings.submitHandler){if(b.submitButton)var f=c(" ").attr("name",
+b.submitButton.name).val(b.submitButton.value).appendTo(b.currentForm);b.settings.submitHandler.call(b,b.currentForm);b.submitButton&&f.remove();return false}return true}b.settings.debug&&d.preventDefault();if(b.cancelSubmit){b.cancelSubmit=false;return e()}if(b.form()){if(b.pendingRequest){b.formSubmitted=true;return false}return e()}else{b.focusInvalid();return false}})}return b}else a&&a.debug&&window.console&&console.warn("nothing selected, can't validate, returning nothing")},valid:function(){if(c(this[0]).is("form"))return this.validate().form();
+else{var a=true,b=c(this[0].form).validate();this.each(function(){a&=b.element(this)});return a}},removeAttrs:function(a){var b={},d=this;c.each(a.split(/\s/),function(e,f){b[f]=d.attr(f);d.removeAttr(f)});return b},rules:function(a,b){var d=this[0];if(a){var e=c.data(d.form,"validator").settings,f=e.rules,g=c.validator.staticRules(d);switch(a){case "add":c.extend(g,c.validator.normalizeRule(b));f[d.name]=g;if(b.messages)e.messages[d.name]=c.extend(e.messages[d.name],b.messages);break;case "remove":if(!b){delete f[d.name];
+return g}var h={};c.each(b.split(/\s/),function(j,i){h[i]=g[i];delete g[i]});return h}}d=c.validator.normalizeRules(c.extend({},c.validator.metadataRules(d),c.validator.classRules(d),c.validator.attributeRules(d),c.validator.staticRules(d)),d);if(d.required){e=d.required;delete d.required;d=c.extend({required:e},d)}return d}});c.extend(c.expr[":"],{blank:function(a){return!c.trim(""+a.value)},filled:function(a){return!!c.trim(""+a.value)},unchecked:function(a){return!a.checked}});c.validator=function(a,
+b){this.settings=c.extend(true,{},c.validator.defaults,a);this.currentForm=b;this.init()};c.validator.format=function(a,b){if(arguments.length==1)return function(){var d=c.makeArray(arguments);d.unshift(a);return c.validator.format.apply(this,d)};if(arguments.length>2&&b.constructor!=Array)b=c.makeArray(arguments).slice(1);if(b.constructor!=Array)b=[b];c.each(b,function(d,e){a=a.replace(RegExp("\\{"+d+"\\}","g"),e)});return a};c.extend(c.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",
+validClass:"valid",errorElement:"label",focusInvalid:true,errorContainer:c([]),errorLabelContainer:c([]),onsubmit:true,ignore:":hidden",ignoreTitle:false,onfocusin:function(a){this.lastActive=a;if(this.settings.focusCleanup&&!this.blockFocusCleanup){this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass);this.addWrapper(this.errorsFor(a)).hide()}},onfocusout:function(a){if(!this.checkable(a)&&(a.name in this.submitted||!this.optional(a)))this.element(a)},
+onkeyup:function(a){if(a.name in this.submitted||a==this.lastElement)this.element(a)},onclick:function(a){if(a.name in this.submitted)this.element(a);else a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).addClass(b).removeClass(d):c(a).addClass(b).removeClass(d)},unhighlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).removeClass(b).addClass(d):c(a).removeClass(b).addClass(d)}},setDefaults:function(a){c.extend(c.validator.defaults,
+a)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",accept:"Please enter a value with a valid extension.",maxlength:c.validator.format("Please enter no more than {0} characters."),
+minlength:c.validator.format("Please enter at least {0} characters."),rangelength:c.validator.format("Please enter a value between {0} and {1} characters long."),range:c.validator.format("Please enter a value between {0} and {1}."),max:c.validator.format("Please enter a value less than or equal to {0}."),min:c.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:false,prototype:{init:function(){function a(e){var f=c.data(this[0].form,"validator"),g="on"+e.type.replace(/^validate/,
+"");f.settings[g]&&f.settings[g].call(f,this[0],e)}this.labelContainer=c(this.settings.errorLabelContainer);this.errorContext=this.labelContainer.length&&this.labelContainer||c(this.currentForm);this.containers=c(this.settings.errorContainer).add(this.settings.errorLabelContainer);this.submitted={};this.valueCache={};this.pendingRequest=0;this.pending={};this.invalid={};this.reset();var b=this.groups={};c.each(this.settings.groups,function(e,f){c.each(f.split(/\s/),function(g,h){b[h]=e})});var d=
+this.settings.rules;c.each(d,function(e,f){d[e]=c.validator.normalizeRule(f)});c(this.currentForm).validateDelegate("[type='text'], [type='password'], [type='file'], select, textarea, [type='number'], [type='search'] ,[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'] ","focusin focusout keyup",a).validateDelegate("[type='radio'], [type='checkbox'], select, option","click",
+a);this.settings.invalidHandler&&c(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)},form:function(){this.checkForm();c.extend(this.submitted,this.errorMap);this.invalid=c.extend({},this.errorMap);this.valid()||c(this.currentForm).triggerHandler("invalid-form",[this]);this.showErrors();return this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(a){this.lastElement=
+a=this.validationTargetFor(this.clean(a));this.prepareElement(a);this.currentElements=c(a);var b=this.check(a);if(b)delete this.invalid[a.name];else this.invalid[a.name]=true;if(!this.numberOfInvalids())this.toHide=this.toHide.add(this.containers);this.showErrors();return b},showErrors:function(a){if(a){c.extend(this.errorMap,a);this.errorList=[];for(var b in a)this.errorList.push({message:a[b],element:this.findByName(b)[0]});this.successList=c.grep(this.successList,function(d){return!(d.name in a)})}this.settings.showErrors?
+this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){c.fn.resetForm&&c(this.currentForm).resetForm();this.submitted={};this.lastElement=null;this.prepareForm();this.hideErrors();this.elements().removeClass(this.settings.errorClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b=0,d;for(d in a)b++;return b},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return this.size()==
+0},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{c(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(a){}},findLastActive:function(){var a=this.lastActive;return a&&c.grep(this.errorList,function(b){return b.element.name==a.name}).length==1&&a},elements:function(){var a=this,b={};return c(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){!this.name&&
+a.settings.debug&&window.console&&console.error("%o has no name assigned",this);if(this.name in b||!a.objectLength(c(this).rules()))return false;return b[this.name]=true})},clean:function(a){return c(a)[0]},errors:function(){return c(this.settings.errorElement+"."+this.settings.errorClass,this.errorContext)},reset:function(){this.successList=[];this.errorList=[];this.errorMap={};this.toShow=c([]);this.toHide=c([]);this.currentElements=c([])},prepareForm:function(){this.reset();this.toHide=this.errors().add(this.containers)},
+prepareElement:function(a){this.reset();this.toHide=this.errorsFor(a)},check:function(a){a=this.validationTargetFor(this.clean(a));var b=c(a).rules(),d=false,e;for(e in b){var f={method:e,parameters:b[e]};try{var g=c.validator.methods[e].call(this,a.value.replace(/\r/g,""),a,f.parameters);if(g=="dependency-mismatch")d=true;else{d=false;if(g=="pending"){this.toHide=this.toHide.not(this.errorsFor(a));return}if(!g){this.formatAndAdd(a,f);return false}}}catch(h){this.settings.debug&&window.console&&console.log("exception occured when checking element "+
+a.id+", check the '"+f.method+"' method",h);throw h;}}if(!d){this.objectLength(b)&&this.successList.push(a);return true}},customMetaMessage:function(a,b){if(c.metadata){var d=this.settings.meta?c(a).metadata()[this.settings.meta]:c(a).metadata();return d&&d.messages&&d.messages[b]}},customMessage:function(a,b){var d=this.settings.messages[a];return d&&(d.constructor==String?d:d[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+a.name+"")},formatAndAdd:function(a,b){var d=this.defaultMessage(a,b.method),e=/\$?\{(\d+)\}/g;if(typeof d=="function")d=d.call(this,b.parameters,a);else if(e.test(d))d=jQuery.format(d.replace(e,"{$1}"),b.parameters);this.errorList.push({message:d,element:a});this.errorMap[a.name]=d;this.submitted[a.name]=
+d},addWrapper:function(a){if(this.settings.wrapper)a=a.add(a.parent(this.settings.wrapper));return a},defaultShowErrors:function(){for(var a=0;this.errorList[a];a++){var b=this.errorList[a];this.settings.highlight&&this.settings.highlight.call(this,b.element,this.settings.errorClass,this.settings.validClass);this.showLabel(b.element,b.message)}if(this.errorList.length)this.toShow=this.toShow.add(this.containers);if(this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);
+if(this.settings.unhighlight){a=0;for(b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass)}this.toHide=this.toHide.not(this.toShow);this.hideErrors();this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return c(this.errorList).map(function(){return this.element})},showLabel:function(a,b){var d=this.errorsFor(a);if(d.length){d.removeClass(this.settings.validClass).addClass(this.settings.errorClass);
+d.attr("generated")&&d.html(b)}else{d=c("<"+this.settings.errorElement+"/>").attr({"for":this.idOrName(a),generated:true}).addClass(this.settings.errorClass).html(b||"");if(this.settings.wrapper)d=d.hide().show().wrap("<"+this.settings.wrapper+"/>").parent();this.labelContainer.append(d).length||(this.settings.errorPlacement?this.settings.errorPlacement(d,c(a)):d.insertAfter(a))}if(!b&&this.settings.success){d.text("");typeof this.settings.success=="string"?d.addClass(this.settings.success):this.settings.success(d)}this.toShow=
+this.toShow.add(d)},errorsFor:function(a){var b=this.idOrName(a);return this.errors().filter(function(){return c(this).attr("for")==b})},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(a){if(this.checkable(a))a=this.findByName(a.name).not(this.settings.ignore)[0];return a},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(a){var b=this.currentForm;return c(document.getElementsByName(a)).map(function(d,
+e){return e.form==b&&e.name==a&&e||null})},getLength:function(a,b){switch(b.nodeName.toLowerCase()){case "select":return c("option:selected",b).length;case "input":if(this.checkable(b))return this.findByName(b.name).filter(":checked").length}return a.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):true},dependTypes:{"boolean":function(a){return a},string:function(a,b){return!!c(a,b.form).length},"function":function(a,b){return a(b)}},optional:function(a){return!c.validator.methods.required.call(this,
+c.trim(a.value),a)&&"dependency-mismatch"},startRequest:function(a){if(!this.pending[a.name]){this.pendingRequest++;this.pending[a.name]=true}},stopRequest:function(a,b){this.pendingRequest--;if(this.pendingRequest<0)this.pendingRequest=0;delete this.pending[a.name];if(b&&this.pendingRequest==0&&this.formSubmitted&&this.form()){c(this.currentForm).submit();this.formSubmitted=false}else if(!b&&this.pendingRequest==0&&this.formSubmitted){c(this.currentForm).triggerHandler("invalid-form",[this]);this.formSubmitted=
+false}},previousValue:function(a){return c.data(a,"previousValue")||c.data(a,"previousValue",{old:null,valid:true,message:this.defaultMessage(a,"remote")})}},classRuleSettings:{required:{required:true},email:{email:true},url:{url:true},date:{date:true},dateISO:{dateISO:true},dateDE:{dateDE:true},number:{number:true},numberDE:{numberDE:true},digits:{digits:true},creditcard:{creditcard:true}},addClassRules:function(a,b){a.constructor==String?this.classRuleSettings[a]=b:c.extend(this.classRuleSettings,
+a)},classRules:function(a){var b={};(a=c(a).attr("class"))&&c.each(a.split(" "),function(){this in c.validator.classRuleSettings&&c.extend(b,c.validator.classRuleSettings[this])});return b},attributeRules:function(a){var b={};a=c(a);for(var d in c.validator.methods){var e;if(e=d==="required"&&typeof c.fn.prop==="function"?a.prop(d):a.attr(d))b[d]=e;else if(a[0].getAttribute("type")===d)b[d]=true}b.maxlength&&/-1|2147483647|524288/.test(b.maxlength)&&delete b.maxlength;return b},metadataRules:function(a){if(!c.metadata)return{};
+var b=c.data(a.form,"validator").settings.meta;return b?c(a).metadata()[b]:c(a).metadata()},staticRules:function(a){var b={},d=c.data(a.form,"validator");if(d.settings.rules)b=c.validator.normalizeRule(d.settings.rules[a.name])||{};return b},normalizeRules:function(a,b){c.each(a,function(d,e){if(e===false)delete a[d];else if(e.param||e.depends){var f=true;switch(typeof e.depends){case "string":f=!!c(e.depends,b.form).length;break;case "function":f=e.depends.call(b,b)}if(f)a[d]=e.param!==undefined?
+e.param:true;else delete a[d]}});c.each(a,function(d,e){a[d]=c.isFunction(e)?e(b):e});c.each(["minlength","maxlength","min","max"],function(){if(a[this])a[this]=Number(a[this])});c.each(["rangelength","range"],function(){if(a[this])a[this]=[Number(a[this][0]),Number(a[this][1])]});if(c.validator.autoCreateRanges){if(a.min&&a.max){a.range=[a.min,a.max];delete a.min;delete a.max}if(a.minlength&&a.maxlength){a.rangelength=[a.minlength,a.maxlength];delete a.minlength;delete a.maxlength}}a.messages&&delete a.messages;
+return a},normalizeRule:function(a){if(typeof a=="string"){var b={};c.each(a.split(/\s/),function(){b[this]=true});a=b}return a},addMethod:function(a,b,d){c.validator.methods[a]=b;c.validator.messages[a]=d!=undefined?d:c.validator.messages[a];b.length<3&&c.validator.addClassRules(a,c.validator.normalizeRule(a))},methods:{required:function(a,b,d){if(!this.depend(d,b))return"dependency-mismatch";switch(b.nodeName.toLowerCase()){case "select":return(a=c(b).val())&&a.length>0;case "input":if(this.checkable(b))return this.getLength(a,
+b)>0;default:return c.trim(a).length>0}},remote:function(a,b,d){if(this.optional(b))return"dependency-mismatch";var e=this.previousValue(b);this.settings.messages[b.name]||(this.settings.messages[b.name]={});e.originalMessage=this.settings.messages[b.name].remote;this.settings.messages[b.name].remote=e.message;d=typeof d=="string"&&{url:d}||d;if(this.pending[b.name])return"pending";if(e.old===a)return e.valid;e.old=a;var f=this;this.startRequest(b);var g={};g[b.name]=a;c.ajax(c.extend(true,{url:d,
+mode:"abort",port:"validate"+b.name,dataType:"json",data:g,success:function(h){f.settings.messages[b.name].remote=e.originalMessage;var j=h===true;if(j){var i=f.formSubmitted;f.prepareElement(b);f.formSubmitted=i;f.successList.push(b);f.showErrors()}else{i={};h=h||f.defaultMessage(b,"remote");i[b.name]=e.message=c.isFunction(h)?h(a):h;f.showErrors(i)}e.valid=j;f.stopRequest(b,j)}},d));return"pending"},minlength:function(a,b,d){return this.optional(b)||this.getLength(c.trim(a),b)>=d},maxlength:function(a,
+b,d){return this.optional(b)||this.getLength(c.trim(a),b)<=d},rangelength:function(a,b,d){a=this.getLength(c.trim(a),b);return this.optional(b)||a>=d[0]&&a<=d[1]},min:function(a,b,d){return this.optional(b)||a>=d},max:function(a,b,d){return this.optional(b)||a<=d},range:function(a,b,d){return this.optional(b)||a>=d[0]&&a<=d[1]},email:function(a,b){return this.optional(b)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(a)},
+url:function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},
+date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a))},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(a)},number:function(a,b){return this.optional(b)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 -]+/.test(a))return false;var d=0,e=0,f=false;a=a.replace(/\D/g,"");for(var g=a.length-1;g>=
+0;g--){e=a.charAt(g);e=parseInt(e,10);if(f)if((e*=2)>9)e-=9;d+=e;f=!f}return d%10==0},accept:function(a,b,d){d=typeof d=="string"?d.replace(/,/g,"|"):"png|jpe?g|gif";return this.optional(b)||a.match(RegExp(".("+d+")$","i"))},equalTo:function(a,b,d){d=c(d).unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){c(b).valid()});return a==d.val()}}});c.format=c.validator.format})(jQuery);
+(function(c){var a={};if(c.ajaxPrefilter)c.ajaxPrefilter(function(d,e,f){e=d.port;if(d.mode=="abort"){a[e]&&a[e].abort();a[e]=f}});else{var b=c.ajax;c.ajax=function(d){var e=("port"in d?d:c.ajaxSettings).port;if(("mode"in d?d:c.ajaxSettings).mode=="abort"){a[e]&&a[e].abort();return a[e]=b.apply(this,arguments)}return b.apply(this,arguments)}}})(jQuery);
+(function(c){!jQuery.event.special.focusin&&!jQuery.event.special.focusout&&document.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.handle.call(this,e)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)},handler:function(e){arguments[0]=c.event.fix(e);arguments[0].type=b;return c.event.handle.apply(this,arguments)}}});c.extend(c.fn,{validateDelegate:function(a,
+b,d){return this.bind(b,function(e){var f=c(e.target);if(f.is(a))return d.apply(f,arguments)})}})})(jQuery);
diff --git a/app/assets/javascripts/store.js b/app/assets/javascripts/store.js
new file mode 100644
index 0000000..f17a0f7
--- /dev/null
+++ b/app/assets/javascripts/store.js
@@ -0,0 +1,48 @@
+function correctPNG() // correctly handle PNG transparency in Win IE 5.5 & 6.
+{
+ var arVersion = navigator.appVersion.split("MSIE")
+ var version = parseFloat(arVersion[1])
+ if ((version >= 5.5) && (document.body.filters))
+ {
+ for(var i=0; i"
+ img.outerHTML = strNewHTML
+ i = i-1
+ }
+ }
+ }
+}
+
+function setup_help_values() {
+ for (element_id in HELP_VALUES) {
+ setup_help_value(element_id);
+ }
+}
+
+function setup_help_value(element_id) {
+ var element = $(element_id).get(0);
+ var help_text = HELP_VALUES[element_id];
+ if (element.value == help_text) {
+ element.value = '';
+ element.style.color = '#000';
+ }
+ else if (element.value == '') {
+ element.style.color = '#aaa';
+ element.value = help_text;
+ }
+}
diff --git a/app/assets/stylesheets/admin.css b/app/assets/stylesheets/admin.css
new file mode 100644
index 0000000..8734e52
--- /dev/null
+++ b/app/assets/stylesheets/admin.css
@@ -0,0 +1,109 @@
+body
+{
+ color: #222;
+}
+
+h1
+{
+ margin-top: 40px;
+}
+
+h2
+{
+ margin: 5px 0 10px;
+ font-size: 16px;
+ border-bottom: 1px dotted #ccc;
+}
+
+#topmenu
+{
+ margin: 0; padding: 0;
+ padding-left: 0;
+ }
+
+#topmenu li
+{
+ height: 40px;
+ font-family: arial, "Trebuchet MS", "Lucida Grande", sans-serif;
+ font-size: 13px;
+ padding-left: 14px;
+ padding-right: 14px;
+ list-style: none;
+ display: table-cell !Important;
+ vertical-align: bottom;
+ border-right: 1px dotted #ddd;
+ border-left: 1px dotted #f5f5f5;
+ display: inline;
+}
+
+#topmenu li:first-child
+{
+ border-left: 1px dotted #ddd;
+ }
+
+
+#submenu
+{
+ margin: 0; padding: 0;
+ padding-left: 0;
+ }
+
+#submenu li
+{
+ height: 18px;
+ font-family: arial, "Trebuchet MS", "Lucida Grande", sans-serif;
+ font-size: 13px;
+ padding-left: 14px;
+ padding-right: 14px;
+ list-style: none;
+ display: table-cell !Important;
+ vertical-align: bottom;
+ border-right: 1px dotted #ddd;
+ border-left: 1px dotted #f5f5f5;
+ display: inline;
+}
+
+#submenu li:first-child
+{
+ border-left: 1px dotted #ddd;
+ }
+
+hr { height: 20px; border: 0; }
+
+/* First column in a table */
+table.form tr td:first-child { text-align: right; }
+
+tr { vertical-align: top; }
+
+#search form
+{
+ margin: 1px 0 0 0;
+}
+
+.pagination
+{
+ margin: 20px 0;
+
+}
+
+/* Used for the order and product list */
+table.list { border-collapse: collapse; }
+table.list th { border:1px dotted #bbb; background-color: #f5f5f5; padding:5px; }
+table.list td { border:1px dotted #999; padding:5px; }
+table.list tr.F, tr.F A { color:#999; }
+
+table.list tr.X { background-color:#e5e5e5; }
+table.list tr.X td { border:1px dotted #aaa; }
+
+table.list tr.R { background-color:#ccc; }
+table.list tr.R td { border:1px dotted #aaa; }
+
+table.list .address { width:150px; }
+table.list tr.newday { border-top:3px double #ccc; }
+
+.fl { float: left; }
+.fr { float: right; }
+
+.sosumi { font-size:10px; color:#555; }
+.red { color:red; }
+.bold { font-weight:bold; }
\ No newline at end of file
diff --git a/app/assets/stylesheets/admin/coupons.css b/app/assets/stylesheets/admin/coupons.css
new file mode 100644
index 0000000..afad32d
--- /dev/null
+++ b/app/assets/stylesheets/admin/coupons.css
@@ -0,0 +1,4 @@
+/*
+ Place all the styles related to the matching controller here.
+ They will automatically be included in application.css.
+*/
diff --git a/app/assets/stylesheets/mytheme.css b/app/assets/stylesheets/mytheme.css
new file mode 100644
index 0000000..77b2b99
--- /dev/null
+++ b/app/assets/stylesheets/mytheme.css
@@ -0,0 +1,283 @@
+body
+{
+ margin: 0; padding: 0;
+ font-family: helvetica, sans-serif;
+ font-size: 12px;
+ color: #333;
+}
+
+a
+{
+ text-decoration: none;
+}
+
+a img
+{
+ border: none;
+}
+
+ul
+{
+ padding: 0 0 0 20px;
+ list-style-type: circle;
+}
+
+li
+{
+ line-height: 1.5em;
+ margin-bottom: 2px;
+}
+
+#copyright
+{
+ font-size: 11px;
+ color: #888;
+ padding: 10px 0 0; margin: 0;
+ height: 57px;
+ clear: both;
+}
+
+#footer p
+{
+ margin: 0;
+ padding: 8px 0 0;
+ text-align: center;
+}
+
+#footer #copyright a
+{
+ color: #888;
+}
+
+#copyright strong
+{
+ border: 1px dotted #555;
+ padding: 3px;
+}
+
+hr
+{
+ height: 1px;
+ display: block;
+ border: 0;
+ clear: both;
+ color: #ccc;
+ background-color: #ccc;
+ margin: 18px 0;
+}
+
+.clear
+{
+ display: block !important;
+ display: none;
+ height: 1px; margin: 0; padding: 0; border: 0;
+ clear: both;
+ background-color: transparent;
+}
+
+.float_right
+{
+ float: right;
+}
+
+.align_center
+{
+ text-align: center;
+}
+
+.xsmall { font-size: 11px; }
+.small { font-size: 12px; }
+.medium { font-size: medium; }
+.large { font-size: 16px; }
+.xlarge { font-size: 20px; }
+
+.lfloat { float: left; }
+.rfloat { float: right; }
+
+/* -------------------------------------------------------------------------------- */
+/* Common theme elements */
+
+#page
+{
+ margin: 0; padding: 0;
+ padding-top: 1px;
+}
+
+#page a { color: #2b627f; }
+#page a:hover { color: #2b627f; text-decoration: underline; }
+
+
+#page_top
+{
+ width: 902px;
+ margin: 0 auto;
+ padding: 0;
+}
+
+#page_top h1
+{
+ color: #ccc;
+ margin: 0;
+ padding: 40px 0 10px 0;
+ height: 50px;
+ font-size: 45px;
+ font-weight: normal;
+ border: 0;
+}
+
+#page_top h1 small
+{
+ font-size: 22px;
+ font-weight: normal;
+ color: #999;
+ position: relative;
+ left: 10px;
+ top: -6px;
+}
+
+#page_top h1 a
+{
+ color: black;
+}
+
+#page_top #logo
+{
+ float: right;
+ position: relative; /* without this the z-index does not work */
+ z-index: 1;
+}
+
+#page_top .tm
+{
+ border-top: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ width: 878px;
+ height: 22px;
+ float: left;
+}
+
+#page_top .tr
+{
+ width: 23px;
+ height: 23px;
+ float: left;
+}
+
+#page_content
+{
+ clear:both;
+ margin: 0 auto 0;
+ width: 810px;
+ padding: 10px 45px 40px !important;
+ padding: 10px 42px 40px;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ line-height: 1.3em;
+}
+
+#page.narrow #page_top
+{
+ width: 732px;
+}
+
+#page.narrow #page_top .tm
+{
+ width: 708px;
+}
+
+#page.narrow #page_content
+{
+ width: 640px;
+}
+
+#page.narrow #footer
+{
+ margin: 0 auto;
+ width: 730px;
+ color: #666;
+}
+
+#page_content_container
+{
+ margin: 0 auto;
+ width: 500px;
+}
+
+/* -------------------------------------------------------------------------------- */
+/* Left column */
+
+#page_content #content_title
+{
+ margin: 0 0 10px; padding: 0;
+ text-align: left;
+}
+
+#page_content .ccolumn.narrow
+{
+ float: left;
+ width: 550px !important;
+ width: 557px;
+ margin-right: 26px;
+}
+
+#page_content .ccolumn.narrow.solo
+{
+ float: none;
+ margin: 0 auto;
+}
+
+#page_content .ccolumn h2
+{
+ font-size: 14px;
+ margin-top: 25px;
+}
+
+#page_content .ccolumn p
+{
+ line-height: 1.4em;
+}
+
+#page_content .ccolumn.wide
+{
+ width: 100%;
+}
+
+#page_content .lcolumn
+{
+ float: left;
+ width: 130px;
+ padding-right: 20px;
+ border-right: 1px dotted #ccc;
+}
+
+#page_content .rcolumn
+{
+ float: left;
+ width: 212px;
+ padding-left: 20px;
+ border-left: 1px dotted #ccc;
+}
+
+#page_content .rcolumn h3 { margin: 0; padding: 0; color: #808080; font-size: 14px; }
+
+#page_bottom
+{
+ margin-top: -20px;
+ position: relative;
+ height: 20px;
+}
+
+#page a.button
+{
+ font-weight: bold;
+ background-color: #aaa;
+ color: #fff;
+ padding: 4px 8px 3px;
+}
+
+ul.links
+{
+ margin: 0;
+ padding: 0;
+}
diff --git a/app/assets/stylesheets/scaffold.css b/app/assets/stylesheets/scaffold.css
new file mode 100644
index 0000000..1ae7000
--- /dev/null
+++ b/app/assets/stylesheets/scaffold.css
@@ -0,0 +1,56 @@
+body { background-color: #fff; color: #333; }
+
+body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a { color: #000; }
+a:visited { color: #666; }
+a:hover { color: #fff; background-color:#000; }
+
+div.field, div.actions {
+ margin-bottom: 10px;
+}
+
+#notice {
+ color: green;
+}
+
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#error_explanation {
+ width: 450px;
+ border: 2px solid red;
+ padding: 7px;
+ padding-bottom: 0;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#error_explanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ margin-bottom: 0px;
+ background-color: #c00;
+ color: #fff;
+}
+
+#error_explanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
diff --git a/app/assets/stylesheets/store.css b/app/assets/stylesheets/store.css
new file mode 100644
index 0000000..7edd66f
--- /dev/null
+++ b/app/assets/stylesheets/store.css
@@ -0,0 +1,233 @@
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+div.uploadStatus {
+ margin: 5px;
+}
+
+div.progressBar {
+ margin: 5px;
+}
+
+div.progressBar div.border {
+ background-color: #fff;
+ border: 1px solid grey;
+ width: 100%;
+}
+
+div.progressBar div.background {
+ background-color: #333;
+ height: 18px;
+ width: 0%;
+}
+
+
+/* Potion Store additions */
+
+#content { margin: 0 auto; }
+
+h1 {
+ margin: 30px 0 40px;
+ font-family: helvetica, sans-serif;
+ font-weight: bold;
+ font-size: 28px;
+ color: #333;
+}
+
+
+h1 span {
+ font-family: helvetica, sans-serif;
+ font-weight: normal;
+ font-size: 28px;
+ color: #555;
+}
+
+h2 {
+ margin: 20px 0 30px;
+ font-family: helvetica, arial, sans-serif;
+ text-transform: uppercase;
+}
+
+h3 {
+ margin-top: 40px;
+}
+
+input.qty, #order_zipcode { vertical-align: middle; }
+input.qty {
+ font-size: 24px;
+ font-weight: bold;
+ text-align: right;
+ color: #444;
+}
+
+#products
+{
+ margin: 40px auto 0;
+}
+#products td:first-child
+{
+ font-size: 18px;
+ font-weight: bold;
+ text-align: right;
+ color: #333;
+}
+
+#products td { padding-right:10px; }
+#products td span { color:#aaa; font-size:10px; }
+#products td strong { font-size:13px; }
+
+#products tr#coupon_row td {
+ font-size:12px;
+ font-weight:normal;
+ padding-top:5px;
+ padding-bottom:0;
+}
+
+#products tr#coupon_row input { width:173px; }
+
+#products #payment_method td { font-size:12px; font-weight:normal; }
+#products #payment_method td:first-child { vertical-align:top; }
+#products #payment_method td p:first-child { margin-top: 0; }
+#products #payment_method br { margin-top:5px; }
+#products input#submit { margin:10px 0; }
+#products #payment_method img { margin-right:3px; }
+
+#products img { vertical-align:middle; margin-right:5px; }
+
+#discounts {
+ border: 1px dotted #ccc;
+ padding: 7px;
+ color: #444;
+ background-color: #ffd;
+}
+
+#site_license {
+ margin: 5px auto;
+ padding: 7px;
+ color: #444;
+}
+
+.fl { float:left }
+.cl { clear:left }
+div.s { height:1px; margin:10px 0 0 137px; }
+div.d { height:1px; margin:20px 0 10px; border-top:1px dotted #999; }
+
+
+/* -------------------------------------------------------------------------------- */
+/* Stuff for the Payment screen */
+
+#page.narrow .ccolumn.narrow.solo div.narrow {
+ width: 440px; margin: 0 auto;
+}
+
+table#order {
+ width: 440px;
+ border-collapse: collapse;
+ margin: 0 auto;
+}
+
+table#order td { margin:0; padding:0 0 7px; }
+table#order .price { width:130px; text-align:right; padding-right:8px; }
+table#order .prod { font-weight:bold; font-size:12px; }
+table#order #total td { padding-top: 10px; font-size:14px; }
+
+span, label, .label {
+ font-family: helvetica, arial, sans-serif;
+ font-size: 12px;
+ color: #333;
+}
+
+p span {
+ position: relative;
+ top: 3px;
+ float: left;
+ width:130px;
+ margin-right: 7px;
+ text-align: right;
+}
+
+#fname, #lname {
+ float: left;
+ font-size: 10px;
+ color: #aaa;
+}
+
+#cards img { vertical-align: middle; margin-right: 7px; }
+#Visa, #MasterCard, #Amex, #Discover { display: none; }
+
+input#order_company,
+input#order_address1,
+input#order_address2,
+input#order_licensee_name,
+input#order_email,
+input#order_comment {
+ width:238px;
+}
+
+input { margin-left: 0; }
+
+#lname, #countries { margin-left: 10px; }
+#order_city { margin-right: 11px; }
+#order_state { margin-left: 7px; }
+#countries { margin-left: 0; }
+#company { margin-top: 0; padding-top:10px; }
+
+#order_cc_month, #order_cc_year { width: 30px; text-align: center; }
+
+#order_cc_month { margin-right: 5px; }
+#order_cc_year { margin-left: 5px; margin-right: 10px; }
+#ccexp img { vertical-align: middle; position: relative; top: -3px; }
+
+input#submit { margin: 10px 0 0 192px; }
+
+#final_step { color: #999; font-family:helvetica, sans-serif; font-size: 11px; margin-left: 15px; }
+
+#errors {
+ padding-bottom: 20px;
+}
+
+#error {
+ color:#d00;
+}
+
+div.fieldWithErrors {
+ padding-right: 13px;
+ background-image: url(/images/store/missing_field.png);
+ background-position: right;
+ background-repeat: no-repeat;
+ display:inline;
+}
+
+/* jQuery Validate plugin error labels */
+label.error {
+ margin:5px 0 0 137px;
+ color:red;
+ font-size:8pt;
+}
+
+/* -------------------------------------------------------------------------------- */
+/* Thank you page */
+
+#license_keys td.first {
+ text-align: right;
+}
+
+.box {
+ padding: 0 15px 10px;
+ border: 1px dotted #ccc;
+ background-color: #ffd;
+}
+
+#license_keys td {
+ padding: 10px 10px 0 0;
+}
+
+#license_keys code {
+ color: black;
+}
+
+#print { width:100%; text-align: right; margin: 20px 0; }
+#print img { vertical-align: top; }
diff --git a/app/controllers/admin/charts_controller.rb b/app/controllers/admin/charts_controller.rb
new file mode 100644
index 0000000..cb69f3f
--- /dev/null
+++ b/app/controllers/admin/charts_controller.rb
@@ -0,0 +1,214 @@
+class Admin::ChartsController < ApplicationController
+
+ #before_filter :redirect_to_ssl, :check_authentication
+
+ def revenue_history_days
+ limit = 30
+ query_results = Order.connection.select_all(revenue_history_days_sql(limit))
+
+ labels = []
+ data = []
+
+ 0.upto(limit-1) {
+ labels << ''
+ data << 0
+ }
+
+ query_results.each {|x|
+ xindex = -x['days_ago'].to_i + limit-1
+ next if xindex < 0 || xindex > limit-1
+ revenue = x['revenue'].to_f.round
+
+ d = Date.strptime("#{x['month']}/#{x['day']}/#{x['year']}", '%m/%d/%Y')
+ labels[xindex] = d.wday == 0 ? "#{x['month']}/#{x['day']}" : ""
+ data[xindex] = revenue
+ }
+
+ g = new_chart(data, labels)
+
+ render :text => g.render
+ end
+
+ def revenue_history_weeks
+ limit = 26
+ query_results = Order.connection.select_all(revenue_history_weeks_sql(limit))
+
+ labels = []
+ data = []
+
+ 0.upto(limit-1) {
+ labels << ''
+ data << 0
+ }
+
+ today = Date.today
+
+ query_results.each {|x|
+ weeks_ago = today.cweek - x['week'].to_i
+ xindex = -weeks_ago + limit-1
+ next if xindex < 0 || xindex > limit-1
+ revenue = x['revenue'].to_f.round
+
+ labels[xindex] = x['week']
+ data[xindex] = revenue
+ }
+
+ g = new_chart(data, labels)
+ g.set_x_label_style(10, '#000000', 0, 2)
+
+ render :text => g.render
+ end
+
+ def revenue_history_months
+ limit = 12
+ query_results = Order.connection.select_all(revenue_history_months_sql(limit))
+
+ labels = []
+ data = []
+
+ 0.upto(limit-1) {
+ labels << ''
+ data << 0
+ }
+
+ query_results.each {|x|
+ xindex = -x['months_ago'].to_i + limit-1
+ next if xindex < 0 || xindex > limit-1
+ revenue = x['revenue'].to_f.round
+
+ labels[xindex] = "#{x['month']}/#{x['year']}"
+ data[xindex] = revenue
+ }
+
+ g = new_chart(data, labels)
+ g.set_x_label_style(10, '#000000', 0, 2)
+
+ render :text => g.render
+ end
+
+ private
+ def new_chart(data, labels)
+ g = OpenFlashChart.new
+
+ g.set_data(data)
+ g.bar_filled(75, '#000000', '#333333', 'revenue', 10)
+ g.set_bg_color('#FFFFFF')
+
+ g.set_x_labels(labels)
+ g.set_x_label_style(10, '#000000', 0, 1)
+ g.set_x_axis_steps(0)
+
+ if data.max == 0
+ g.set_y_max(100)
+ else
+ g.set_y_max((data.max.to_f/100).ceil * 100)
+ end
+
+ g.set_y_label_steps(5)
+
+ g.instance_variable_set :@y_axis_color, '#AAAAAA'
+ g.instance_variable_set :@x_axis_color, '#666666'
+ g.instance_variable_set :@y_grid_color, '#CCCCCC'
+ g.instance_variable_set :@x_grid_color, '#FFFFFF'
+
+ g.set_tool_tip('#x_label# $#val#');
+
+ return g
+ end
+
+ private
+ def revenue_history_days_sql(days)
+ if Order.connection.adapter_name.downcase == 'mysql'
+ "select extract(year from orders.order_time) as year,
+ extract(month from orders.order_time) as month,
+ extract(day from orders.order_time) as day,
+ datediff(now(), orders.order_time) as days_ago,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0 and current_date - #{days-1} <= order_time
+
+ group by year, month, day, days_ago
+
+ order by last_time desc"
+ else
+ "select extract(year from orders.order_time) as year,
+ extract(month from orders.order_time) as month,
+ extract(day from orders.order_time) as day,
+ extract(day from age(date_trunc('day', orders.order_time))) as days_ago,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0 and current_date - #{days-1} <= order_time
+
+ group by year, month, day, days_ago
+
+ order by last_time desc"
+ end
+ end
+
+ def revenue_history_weeks_sql(weeks)
+ # This query stays the same for both DBMS
+ if Order.connection.adapter_name.downcase == 'mysql'
+ "select extract(week from orders.order_time) as week,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0 and datediff(current_date(), order_time) <= #{weeks-1}*7
+
+ group by week
+
+ order by last_time desc limit #{weeks}"
+ else
+ "select extract(week from orders.order_time) as week,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0 and current_date - #{(weeks-1)*7} <= order_time
+
+ group by week
+
+ order by last_time desc limit #{weeks}"
+ end
+ end
+
+ def revenue_history_months_sql(months)
+ if Order.connection.adapter_name == 'mysql'
+ "select extract(year from orders.order_time) as year,
+ extract(month from orders.order_time) as month,
+ period_diff( extract( year_month from current_date ), extract( year_month from orders.order_time ) ) as months_ago,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0
+
+ group by year, month, months_ago
+
+ order by last_time desc limit #{months}"
+ else
+ "select extract(year from orders.order_time) as year,
+ extract(month from orders.order_time) as month,
+ extract(month from age(date_trunc('month', orders.order_time))) as months_ago,
+ sum(total) as revenue,
+ max(orders.order_time) as last_time
+
+ from orders
+
+ where status = 'C' and total > 0
+
+ group by year, month, months_ago
+
+ order by last_time desc limit #{months}"
+ end
+ end
+end
diff --git a/app/controllers/admin/coupons_controller.rb b/app/controllers/admin/coupons_controller.rb
new file mode 100644
index 0000000..bcbaf7b
--- /dev/null
+++ b/app/controllers/admin/coupons_controller.rb
@@ -0,0 +1,124 @@
+class Admin::CouponsController < ApplicationController
+ layout "admin"
+ before_filter :check_authentication
+
+ def index
+ @coupons = Coupon.find_by_sql("select count(*) as count, i.code, i.product_code from coupons i group by i.code,i.product_code order by i.product_code,i.code")
+ end
+
+ def show
+ @coupons = Coupon.where(:code => params[:id])
+ end
+
+ def new
+ @coupon = Coupon.new
+ end
+
+ def edit
+ @coupon = Coupon.find(params[:id])
+ end
+
+ def toggle_state
+ @coupon = Coupon.find(params[:id])
+
+ if params[:operation] == 'enable'
+ @coupon.enabled = true
+ elsif params[:operation] == 'disable'
+ @coupon.enabled = false
+ end
+
+ if @coupon.save
+ redirect_to admin_coupon_path(@coupon.code), notice: 'Coupon was successfully disabled.'
+ else
+ redirect_to admin_coupon_path(@coupon.code), notice: 'Unable to disable coupon.'
+ end
+ end
+
+ def toggle_state_for_all_coupons_with_code
+ @coupons = Coupon.where(:code => params[:id])
+ @coupons.each do |coupon|
+ if params[:operation] == 'enable'
+ coupon.enabled = true
+ elsif params[:operation] == 'disable'
+ coupon.enabled = false
+ end
+ coupon.save
+ end
+
+ redirect_to admin_coupon_path(@coupons.first.code)
+ end
+
+=begin
+ def delete_all_coupons_with_code
+ @coupons = Coupon.where(:code => params[:id])
+ @coupons.each do |coupon|
+ coupon.delete
+ end
+
+ redirect_to admin_coupons_path
+ end
+=end
+
+ def create
+ if params[:coupon]
+ form = params[:coupon]
+
+ if ! form["expiration_date(1i)"].blank?
+ expiration_date = Time.new(form["expiration_date(1i)"].to_i,
+ form["expiration_date(2i)"].to_i,
+ form["expiration_date(3i)"].to_i,
+ form["expiration_date(4i)"].to_i,
+ form["expiration_date(5i)"].to_i)
+ else
+ expiration_date = nil
+ end
+
+ if Integer(form[:quantity]) == 1 && !form[:coupon].blank?
+ generate_coupon(form[:code], form[:product_code], form[:description],
+ form[:amount], form[:use_limit], form[:coupon].gsub(/[^0-9a-z ]/i, '').upcase,
+ expiration_date)
+ else
+ 1.upto(Integer(form[:quantity])) { |i|
+ generate_coupon(form[:code], form[:product_code], form[:description],
+ form[:amount], form[:use_limit], Coupon.random_string_of_length(16).upcase,
+ expiration_date)
+ }
+ end
+
+ flash[:notice] = 'Coupons generated'
+ end
+
+ redirect_to admin_coupons_path
+ end
+
+ def update
+ @coupon = Coupon.find(params[:id])
+
+ if @coupon.update_attributes(params[:coupon])
+ redirect_to admin_coupons_path, notice: 'Coupon was successfully updated.'
+ else
+ render action: "edit"
+ end
+ end
+
+ def destroy
+ @coupon = Coupon.find(params[:id])
+ @coupon.destroy
+
+ redirect_to admin_coupons_url
+ end
+
+ private
+ def generate_coupon(code, product_code, description, amount, use_limit, coupon_code, expiration_date)
+ coupon = Coupon.new
+ coupon.code = code
+ coupon.product_code = product_code
+ coupon.description = description
+ coupon.amount = amount
+ coupon.use_limit = use_limit
+ coupon.coupon = coupon_code
+ coupon.creation_time = Time.now
+ coupon.expiration_date = expiration_date
+ coupon.save
+ end
+end
diff --git a/app/controllers/admin/orders_controller.rb b/app/controllers/admin/orders_controller.rb
new file mode 100644
index 0000000..0c800e9
--- /dev/null
+++ b/app/controllers/admin/orders_controller.rb
@@ -0,0 +1,162 @@
+class Admin::OrdersController < ApplicationController
+ layout "admin"
+ before_filter :redirect_to_ssl, :check_authentication
+
+ # GET /orders
+ # GET /orders.xml
+ def index
+ q = params[:query]
+ conditions = "status <> 'P'"
+ if q
+ q = q.strip().downcase()
+ if q.to_i != 0
+ conditions = [conditions + "AND id=?", q.to_i]
+ else
+ conditions = [conditions + " AND (LOWER(email) LIKE ? OR
+ LOWER(first_name) LIKE ? OR
+ LOWER(last_name) LIKE ? OR
+ LOWER(licensee_name) LIKE ?)", "#{q}%", "#{q}%", "#{q}%", "%#{q}%"]
+ end
+ end
+ @orders = Order.paginate :page => (params[:page] || 1), :per_page => 100, :conditions => conditions, :order => 'order_time DESC'
+
+ respond_to do |format|
+ format.html # index.rhtml
+ format.xml { render :xml => @orders.to_xml }
+ end
+ end
+
+ # GET /orders/1
+ # GET /orders/1.xml
+ def show
+ @order = Order.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.rhtml
+ format.xml { render :xml => @order.to_xml }
+ end
+ end
+
+ # GET /orders/new
+ def new
+ @order = Order.new
+ @order.country = 'US'
+ @order.payment_type = 'Free'
+ end
+
+ # GET /orders/1;edit
+ def edit
+ @order = Order.find(params[:id])
+ end
+
+ # POST /orders
+ # POST /orders.xml
+ def create
+ @order = Order.new(params[:order])
+ @order.status = 'C'
+
+ respond_to do |format|
+ if save()
+ flash[:notice] = 'Order was successfully created.'
+ format.html { redirect_to admin_order_url(@order) }
+ format.xml { head :created, :location => admin_order_url(@order) }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @order.errors.to_xml }
+ end
+ end
+ end
+
+ # PUT /orders/1
+ # PUT /orders/1.xml
+ def update
+ @order = Order.find(params[:order][:id])
+
+ # Delete the id from the form or ActiveRecord complains about changing it
+ params[:order].delete(:id)
+
+ @order.attributes = params[:order]
+
+ if not save()
+ redirect_to :back and return
+ end
+
+ respond_to do |format|
+ if @order.update_attributes(params[:order])
+ flash[:notice] = 'Order was successfully updated.'
+ format.html { redirect_to admin_order_url(@order) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @order.errors.to_xml }
+ end
+ end
+ end
+
+ # DELETE /orders/1
+ # DELETE /orders/1.xml
+ def destroy
+ @order = Order.find(params[:id])
+ @order.destroy
+
+ respond_to do |format|
+ format.html { redirect_to orders_url }
+ format.xml { head :ok }
+ end
+ end
+
+ # GET /orders/1/cancel
+ def cancel
+ @order = Order.find(params[:id])
+ @order.status = 'X'
+ @order.save
+ redirect_to :action => 'show', :id => @order.id
+ end
+
+ # GET /orders/1/uncancel
+ def uncancel
+ @order = Order.find(params[:id])
+ @order.status = 'C'
+ @order.save
+ redirect_to :action => 'show', :id => @order.id
+ end
+
+ # GET /orders/1/refund
+ def refund
+ @order = Order.find(params[:id])
+ @order.refund
+ redirect_to :action => 'show', :id => @order.id
+ end
+
+ # GET /orders/1/send_emails
+ def send_emails
+ @order = Order.find(params[:id])
+ OrderMailer.thankyou(@order).deliver
+ redirect_to :action => 'show', :id => @order.id
+ end
+
+ protected
+ def save
+ begin
+ @order.transaction do
+ if not @order.add_or_update_items(params[:items])
+ flash[:notice] = 'Order contains no items'
+ logger.warn('Order contains no items')
+ return false
+ end
+ @order.update_item_prices(params[:item_prices])
+ @order.skip_cc_validation = true
+
+ if not @order.save()
+ flash[:notice] = 'Could not save order'
+ logger.error("ERROR -- Could not save order: #{@order.errors}")
+ return false
+ end
+ return true
+ end
+ rescue Exception => e
+ logger.error("ERROR -- Problem saving order: #{e}")
+ return false
+ end
+ end
+end
diff --git a/app/controllers/admin/products_controller.rb b/app/controllers/admin/products_controller.rb
new file mode 100644
index 0000000..81e9928
--- /dev/null
+++ b/app/controllers/admin/products_controller.rb
@@ -0,0 +1,84 @@
+class Admin::ProductsController < ApplicationController
+ layout "admin"
+
+ #before_filter :redirect_to_ssl, :check_authentication
+
+ # GET /products
+ # GET /products.xml
+ def index
+ @products = Product.find(:all)
+
+ respond_to do |format|
+ format.html # index.rhtml
+ format.xml { render :xml => @products.to_xml }
+ end
+ end
+
+ # GET /products/1
+ # GET /products/1.xml
+ def show
+ @product = Product.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.rhtml
+ format.xml { render :xml => @product.to_xml }
+ end
+ end
+
+ # GET /products/new
+ def new
+ @product = Product.new
+ end
+
+ # GET /products/1;edit
+ def edit
+ @product = Product.find(params[:id])
+ end
+
+ # POST /products
+ # POST /products.xml
+ def create
+ @product = Product.new(params[:product])
+
+ respond_to do |format|
+ if @product.save
+ flash[:notice] = 'Product was successfully created.'
+ format.html { redirect_to admin_product_url(@product) }
+ format.xml { head :created, :location => admin_product_url(@product) }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @product.errors.to_xml }
+ end
+ end
+ end
+
+ # PUT /products/1
+ # PUT /products/1.xml
+ def update
+ @product = Product.find(params[:id])
+
+ respond_to do |format|
+ if @product.update_attributes(params[:product])
+ flash[:notice] = 'Product was successfully updated.'
+ format.html { redirect_to admin_product_url(@product) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @product.errors.to_xml }
+ end
+ end
+ end
+
+ # DELETE /products/1
+ # DELETE /products/1.xml
+ def destroy
+ @product = Product.find(params[:id])
+ @product.destroy
+
+ respond_to do |format|
+ format.html { redirect_to admin_products_url }
+ format.xml { head :ok }
+ end
+ end
+
+end
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
new file mode 100644
index 0000000..3f38cb3
--- /dev/null
+++ b/app/controllers/admin_controller.rb
@@ -0,0 +1,284 @@
+class AdminController < ApplicationController
+ # Authentication stuff
+ before_filter :redirect_to_ssl
+ before_filter :check_authentication, :except => [:login]
+
+ def login
+ unless params[:username] && params[:password]
+ render :action => "login" and return
+ end
+
+ if params[:username] == $STORE_PREFS['admin_username'] &&
+ params[:password] == $STORE_PREFS['admin_password']
+ session[:logged_in] = true
+ if session[:intended_url] != nil
+ redirect_to session[:intended_url]
+ else
+ redirect_to :action => 'index'
+ end
+ else
+ flash[:notice] = "Go home kid. This ain't for you."
+ render :action => "login"
+ end
+ end
+
+ def logout
+ session[:logged_in] = nil
+ redirect_to home_url
+ end
+
+ # Dashboard page
+ def index
+ if Product.count == 0
+ flash[:notice] = "This store doesn't have any products! Add some!"
+ redirect_to :action => 'products' and return
+ end
+
+ revenue_summary()
+
+ flashRoute = url_for :controller => 'admin/charts', :action => 'revenue_history_days'
+ @chart = OpenFlashChart.swf_object(500, 170, flashRoute)
+ end
+
+ # The revenue_xxx functions get called through ajax when user chooses different types of reports
+ def revenue_summary_amount
+ revenue_summary()
+ @type = "amount"
+ render :partial => "revenue_summary"
+ end
+
+ def revenue_summary_quantity
+ revenue_summary()
+ @type = "quantity"
+ render :partial => "revenue_summary"
+ end
+
+ def revenue_summary_percentage
+ revenue_summary()
+ @type = "percentage"
+ render :partial => "revenue_summary"
+ end
+
+ def revenue_history_days
+ @type = "30 Day"
+ flashRoute = url_for :controller => 'admin/charts', :action => 'revenue_history_days'
+ @chart = OpenFlashChart.swf_object(500, 170, flashRoute)
+ render :partial => "revenue_history"
+ end
+
+ def revenue_history_weeks
+ @type = "26 Week"
+ flashRoute = url_for :controller => 'admin/charts', :action => 'revenue_history_weeks'
+ @chart = OpenFlashChart.swf_object(500, 170, flashRoute)
+ render :partial => "revenue_history"
+ end
+
+ def revenue_history_months
+ @type = "12 Month"
+ flashRoute = url_for :controller => 'admin/charts', :action => 'revenue_history_months'
+ @chart = OpenFlashChart.swf_object(500, 170, flashRoute)
+ render :partial => "revenue_history"
+ end
+
+ # Coupon actions
+=begin
+ def generate_coupons
+ if params[:form]
+ form = params[:form]
+ @coupons = []
+ 1.upto(Integer(form[:quantity])) { |i|
+ coupon = Coupon.new
+ coupon.code = form[:code]
+ coupon.product_code = form[:product_code]
+ coupon.description = form[:description]
+ coupon.amount = form[:amount]
+ coupon.use_limit = form[:use_limit]
+ coupon.save()
+ @coupons << coupon
+ }
+ flash[:notice] = 'Coupons generated'
+ end
+ end
+=end
+
+# def add_coupons # unused
+# if params[:form]
+# form = params[:form]
+# lines = form[:coupons].split("\r\n")
+# lines.reject! {|x| x.strip == ''}
+# for line in lines
+# coupon = Coupon.new
+# coupon.code = form[:code]
+# coupon.coupon = line.strip()
+# coupon.product_code = 'x'
+# coupon.description = form[:description].strip()
+# coupon.amount = form[:amount].strip()
+# coupon.save()
+# end
+# end
+# end
+
+# def mass_order # unused
+# if params[:form]
+# form = params[:form]
+# for key in form.keys()
+# form[key] = form[key].strip()
+# end
+# lines = form[:people].split("\r\n")
+# lines.reject! {|x| x.strip == ''}
+# for line in lines
+# fname, lname, email = line.split(",").collect{|x| x.strip}
+# order = Order.new
+
+# # add item
+# order.order_time = Time.now()
+
+# order.add_form_items(params[:items])
+# order.update_item_prices(params[:item_prices])
+
+# order.first_name = fname
+# order.last_name = lname
+# order.email = email
+
+# order.address1 = 'n/a'
+# order.address2 = ''
+# order.city = 'n/a'
+# order.state = 'n/a'
+# order.zipcode = 'n/a'
+# order.country = 'XX'
+
+# order.payment_type = form[:payment_type]
+# order.cc_number = 'XXXXXXXXXXXXXXXX'
+# order.cc_month = 'n/a'
+# order.cc_year = 'n/a'
+# order.cc_code = 'n/a'
+
+# order.comment = ''
+
+# order.status = 'C'
+# order.save()
+
+# coupons = order.add_promo_coupons()
+
+# email = OrderMailer.thankyou(order).deliver
+# end
+# end
+# end
+
+ # Revenue summary
+ private
+ def revenue_summary
+ # NOTE: We have to use SQL because performance is completely unacceptable otherwise
+
+ # helper function
+ def last_n_days_sql(days)
+
+ if Order.connection.adapter_name.downcase == 'mysql'
+ "select (select count(*)
+ from orders
+ where status = 'C' and total > 0 and datediff(current_date(), order_time) <= #{days-1}) as orders,
+ sum(unit_price * quantity) as revenue,
+ sum(quantity) as quantity,
+ products.code as product_name
+
+ from orders
+ inner join line_items on orders.id = line_items.order_id
+ left outer join products on products.id = line_items.product_id
+
+ where status = 'C' and total > 0 and datediff(current_date(), order_time) <= #{days-1}
+ group by product_name"
+ else
+ "select (select count(*)
+ from orders
+ where status = 'C' and total > 0 and current_date - #{days-1} <= order_time) as orders,
+ sum(unit_price * quantity) as revenue,
+ sum(quantity) as quantity,
+ products.code as product_name
+
+ from orders
+ inner join line_items on orders.id = line_items.order_id
+ left outer join products on products.id = line_items.product_id
+
+ where status = 'C' and total > 0 and current_date - #{days-1} <= order_time
+ group by product_name"
+ end
+ end
+
+ query_results = []
+ @num_orders = []
+ @revenue = []
+ @product_revenue = {}
+ @product_quantity = {}
+ @product_percentage = {}
+
+ for days in [1, 7, 30, 365]
+ query_results << Order.connection.select_all(last_n_days_sql(days))
+ end
+ @products = query_results[-1].map{|p| p["product_name"]}
+
+ # calculate the numbers to report
+ for result in query_results
+ orders = 0
+ total = 0
+ for row in result
+ name = row["product_name"]
+ name.upcase! if name
+ @product_revenue[name] = [] if @product_revenue[name] == nil
+ @product_quantity[name] = [] if @product_quantity[name] == nil
+ @product_revenue[name] << row["revenue"]
+ @product_quantity[name] << row["quantity"]
+ orders = row["orders"]
+ total = total.to_f + row["revenue"].to_f
+ end
+ @num_orders << orders
+ @revenue << total
+ end
+
+ for product in @products
+ @product_revenue[product].insert(0, 0) while @product_revenue[product].length < 4
+ @product_quantity[product].insert(0, 0) while @product_quantity[product].length < 4
+ @product_percentage[product] = []
+ for i in 0..3
+ if @revenue[i].to_f == 0
+ @product_percentage[product] << 0
+ else
+ @product_percentage[product] << @product_revenue[product][i].to_f / @revenue[i].to_f * 100.0
+ end
+ end
+ end
+
+ def last_n_days_revenue(days)
+ if Order.connection.adapter_name.downcase == 'mysql'
+ last_n_days_sql = "
+ select sum(total) as revenue
+ from orders
+ where status = 'C' and total > 0 and datediff(current_date(), order_time) <= #{days-1}"
+ else
+ last_n_days_sql = "
+ select sum(total) as revenue
+ from orders
+ where status = 'C' and total > 0 and current_date - #{days-1} <= order_time"
+ end
+
+ result = Order.connection.select_all(last_n_days_sql)
+ return (result != nil && result.length > 0 && result[0]["revenue"] != nil) ? result[0]["revenue"] : 0
+ end
+
+ @month_estimate = 0
+ @year_estimate = 0
+
+ daily_avg = last_n_days_revenue(90).to_f / 90.0
+
+ # Calculate a very simple sales projection.
+ # Takes the average daily sales from the last 90 days to extrapolate the sales
+ # for the remaining days of the current month and the next 365 days
+ today = Date.today
+ days_in_current_month = Date.civil(today.year, today.month, -1).day
+
+ if result != nil and result.length > 0
+ @month_estimate = last_n_days_revenue(today.day).to_f + daily_avg * (days_in_current_month - today.day)
+ @year_estimate = daily_avg * 365
+ end
+ end
+
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..5cc1ec6
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,94 @@
+# Filters added to this controller will be run for all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+
+ def check_authentication
+ unless session[:logged_in]
+ session[:intended_url] = request.url
+ logger.debug('intended_url: ' + session[:intended_url])
+ redirect_to :controller => "/admin", :action => "login"
+ end
+ end
+
+ def redirect_to_ssl
+ if is_live? && $STORE_PREFS['redirect_to_ssl']
+ redirect_to :protocol => "https://" unless (request.ssl? or request.local?)
+ end
+ end
+
+end
+
+
+# Convenience global function to check if we're running in production mode
+def is_live?
+ return Rails.env == 'production'
+end
+
+
+# Load store preferences
+def load_store_prefs
+ app_root = File.dirname(__FILE__) + '/../..'
+ config_dir = app_root + '/config/'
+
+ ymlpath = File.expand_path(config_dir + 'store.yml')
+ $STORE_PREFS = YAML.load(File.open(ymlpath))
+end
+
+load_store_prefs()
+
+
+# Convenience global function for rounding to monetary amount
+def round_money(amount)
+ return ("%01.2f" % amount).to_f()
+end
+
+
+# Setup Google Checkout if it's in use
+if $STORE_PREFS['allow_google_checkout']
+
+ require 'google4r/checkout'
+
+ $GCHECKOUT_FRONTEND = nil
+
+ class TaxTableFactory
+ def effective_tax_tables_at(time)
+
+ tax_free_table = Google4R::Checkout::TaxTable.new(false)
+ tax_free_table.name = "default table"
+ tax_free_table.create_rule do |rule|
+ rule.area = Google4R::Checkout::UsCountryArea.new(Google4R::Checkout::UsCountryArea::ALL)
+ rule.rate = 0.0
+ end
+ return [tax_free_table]
+ end
+ end
+
+ def _initialize_google_checkout
+ environment = Rails.env || 'production'
+
+ app_root = File.dirname(__FILE__) + '/../..'
+ config_dir = app_root + '/config'
+
+ prefs = File.expand_path(config_dir + '/google_checkout.yml')
+ if File.exists?(prefs)
+ y = YAML.load(File.open(prefs))[environment]
+
+ # Save the merchant id and key. It gets used in notification_controller for authenticating Google's notifications
+ $STORE_PREFS['gcheckout_merchant_id'] = y['gcheckout_merchant_id']
+ $STORE_PREFS['gcheckout_merchant_key'] = y['gcheckout_merchant_key']
+
+ $GCHECKOUT_FRONTEND = Google4R::Checkout::Frontend.new(:merchant_id => y['gcheckout_merchant_id'],
+ :merchant_key => y['gcheckout_merchant_key'],
+ :use_sandbox => !is_live?())
+
+ $GCHECKOUT_FRONTEND.tax_table_factory = TaxTableFactory.new
+ else
+ logger.error("Could not load Google Checkout configuration even though it's enabled")
+ end
+ end
+
+ # Go ahead and call the Google Checkout initialization function
+ _initialize_google_checkout()
+
+end
diff --git a/app/controllers/store/lost_license_controller.rb b/app/controllers/store/lost_license_controller.rb
new file mode 100644
index 0000000..726622e
--- /dev/null
+++ b/app/controllers/store/lost_license_controller.rb
@@ -0,0 +1,28 @@
+class Store::LostLicenseController < ApplicationController
+ layout "store"
+
+ def index
+ end
+
+ def retrieve
+ if params[:email].blank?
+ flash[:license_notice] = "We can't do much without an email address"
+ render :action => 'index' and return
+ end
+
+ email = params[:email].strip().downcase()
+ orders = Order.find(:all, :conditions => ["status='C' AND LOWER(email)=?", email])
+ if orders.empty?
+ flash[:license_notice] = "Could not find any orders for " + email
+ @email = email
+ render :action => 'index' and return
+ end
+ for order in orders
+ OrderMailer.thankyou(order, bcc = false).deliver
+ if $STORE_PREFS['send_lost_license_sent_notification_email']
+ OrderMailer.lost_license_sent(order).deliver
+ end
+ end
+ redirect_to :action => 'sent'
+ end
+end
diff --git a/app/controllers/store/notification_controller.rb b/app/controllers/store/notification_controller.rb
new file mode 100644
index 0000000..81a1899
--- /dev/null
+++ b/app/controllers/store/notification_controller.rb
@@ -0,0 +1,101 @@
+require 'base64'
+require 'xmlsimple'
+
+def _xmlval(hash, key)
+ if hash[key] == {}
+ nil
+ else
+ hash[key]
+ end
+end
+
+
+class Store::NotificationController < ApplicationController
+
+ ## Google Checkout notification
+
+ def gcheckout
+ # Check HTTP basic authentication first
+ my_auth_key = Base64.encode64($STORE_PREFS['gcheckout_merchant_id'] + ':' + $STORE_PREFS['gcheckout_merchant_key']).strip()
+ http_auth = request.headers['HTTP_AUTHORIZATION']
+ if http_auth.nil? || http_auth.split(' ')[0] != 'Basic' || http_auth.split(' ')[1] != my_auth_key then
+ logger.warn('Got unauthorized Google Checkout notification')
+ render :text => 'Unauthorized', :status => 401 and return
+ end
+
+ # Authenticated. Parse the xml now
+ notification = XmlSimple.xml_in(request.raw_post, 'KeepRoot' => true, 'ForceArray' => false)
+
+ notification_name = notification.keys[0]
+ notification_data = notification[notification_name]
+
+ case notification_name
+ when 'new-order-notification'
+ process_new_order_notification(notification_data)
+
+ when 'charge-amount-notification'
+ process_charge_amount_notification(notification_data)
+ # Ignore the other notifications
+# when 'order-state-change-notification'
+# when 'risk-information-notification'
+ end
+
+ render :text => ''
+ end
+
+ private
+ def process_new_order_notification(n)
+ order = Order.find(Integer(n['shopping-cart']['merchant-private-data']['order-id']))
+
+ return if order == nil or order.payment_type != 'Google Checkout'
+
+ ba = n['buyer-billing-address']
+
+ if ba['structured-name']
+ order.first_name = _xmlval(ba['structured-name'], 'first-name')
+ order.last_name = _xmlval(ba['structured-name'], 'last-name')
+ else
+ words = ba['contact-name'].split(' ')
+ order.first_name = words.shift
+ order.last_name = words.join(' ')
+ end
+
+ order.email = _xmlval(ba, 'email')
+ if order.email == nil # This shouldn't happen, but just in case
+ order.status = 'F'
+ order.failure_reason = 'Did not get email from Google Checkout'
+ order.finish_and_save()
+ return
+ end
+
+ order.address1 = _xmlval(ba, 'address1')
+ order.address2 = _xmlval(ba, 'address2')
+ order.city = _xmlval(ba, 'city')
+ order.company = _xmlval(ba, 'company-name')
+ order.country = _xmlval(ba, 'country-code')
+ order.zipcode = _xmlval(ba, 'postal-code')
+ order.state = _xmlval(ba, 'region')
+
+ order.transaction_number = n['google-order-number']
+
+ order.save()
+
+ order.subscribe_to_list() if n['buyer-marketing-preferences']['email_allowed'] == 'true'
+
+ order.gcheckout_add_merchant_order_number()
+ end
+
+ private
+ def process_charge_amount_notification(n)
+ order = Order.find_by_transaction_number_and_payment_type(n['google-order-number'], 'Google Checkout')
+
+ return if order == nil or order.status == 'C'
+
+ order.status = 'C'
+ order.finish_and_save()
+ OrderMailer.thankyou(order).deliver if is_live?()
+
+ order.gcheckout_archive_order()
+ end
+
+end
diff --git a/app/controllers/store/order_controller.rb b/app/controllers/store/order_controller.rb
new file mode 100644
index 0000000..1d624df
--- /dev/null
+++ b/app/controllers/store/order_controller.rb
@@ -0,0 +1,293 @@
+class Store::OrderController < ApplicationController
+ layout "store"
+
+ before_filter :redirect_to_ssl
+
+ def index
+ new
+ render :action => 'new'
+ end
+
+ def new
+ session[:order_id] = nil
+ @qty = {}
+ @payment_type = session[:payment_type]
+ @products = Product.where(:active => 1)
+ if params[:product]
+ @qty[params[:product]] = 1
+ elsif session[:items]
+ for key in session[:items].keys
+ @qty[Product.find(key).code] = session[:items][key]
+ end
+ end
+ end
+
+ def payment
+ session[:order_id] = nil
+ redirect_to :action => 'index' and return if !params[:items]
+ @order = Order.new
+ @order.payment_type = params[:payment_type]
+ session[:payment_type] = params[:payment_type]
+
+ session[:items] = params[:items]
+
+ if not @order.add_form_items(params[:items])
+ flash[:notice] = 'Nothing to buy!'
+ redirect_to :action => 'index' and return
+ end
+
+ coupon_text = params[:coupon].strip
+ @order.coupon_text = coupon_text
+
+ if !coupon_text.blank? && @order.coupon == nil
+ coupon = Coupon.find_by_coupon(coupon_text)
+ if coupon != nil && coupon.expired?
+ flash[:notice] = 'Coupon Expired'
+ elsif coupon != nil && !coupon.enabled?
+ flash[:notice] = 'Invalid Coupon'
+ else
+ flash[:notice] = 'Invalid Coupon'
+ end
+ session[:coupon_text] = params[:coupon].strip
+ redirect_to :action => 'index' and return
+ end
+
+ if @order.total <= 0
+ flash[:notice] = 'Nothing to buy!'
+ redirect_to :action => 'index' and return
+ end
+
+ if params[:payment_type] == 'paypal'
+ # Handle Paypal orders
+ res = @order.paypal_set_express_checkout(url_for(:action => 'confirm_paypal'), url_for(:action => 'index'))
+
+ if res.ack == 'Success' || res.ack == 'SuccessWithWarning'
+ # Need to copy the string. For some reason, it tries to render the payment action otherwise
+ session[:paypal_token] = String.new(res.token)
+ if not @order.save()
+ flash[:notice] = 'Problem saving order'
+ redirect_to :action => 'index' and return
+ end
+ session[:order_id] = @order.id
+ redirect_to PayPal.express_checkout_redirect_url(res.token)
+ else
+ flash[:notice] = 'Could not connect to PayPal'
+ redirect_to :action => 'index'
+ end
+ elsif params[:payment_type] == 'gcheckout'
+ # Handle Google Checkout orders
+ render :action => 'payment_gcheckout'
+ else
+ # credit card order
+ # put in a dummy credit card number for testing
+ @order.cc_number = '4916306176169494' if not is_live?()
+
+ render :action => 'payment_cc'
+ end
+ end
+
+ def redirect
+ redirect_to :action => 'index'
+ end
+
+ # Accept orders from Cocoa storefront. It only works with JSON right now
+ def create
+ if params[:order] == nil
+ respond_to do |format|
+ format.json { render :json => '["Did not receive order"]', :status => :unprocessable_entity and return }
+ end
+ end
+
+ # If there's a completed order in the session, just return that instead of charging twice
+ if session[:order_id] != nil
+ @order = Order.find(session[:order_id])
+ if @order != nil && @order.status == 'C'
+ respond_to do |format|
+ format.json { render :json => @order.to_json(:include => [:line_items]) }
+ end
+ return
+ end
+ end
+
+ @order = Order.new(params[:order])
+
+ if not @order.save()
+ respond_to do |format|
+ format.json { render :json => @order.errors.full_messages.to_json, :status => :unprocessable_entity }
+ end
+ return
+ end
+
+ session[:order_id] = @order.id
+
+ # Actually send out the payload
+ if @order.cc_order?
+ success = @order.paypal_direct_payment(request)
+ @order.status = success ? 'C' : 'F'
+ @order.finish_and_save() if success
+
+ respond_to do |format|
+ if success
+ format.json { render :json => @order.to_json(:include => [:line_items]) }
+ else
+ format.json { render :json => @order.errors.full_messages.to_json, :status => :unprocessable_entity }
+ end
+ end
+ end
+ end
+
+ def purchase
+ redirect_to :action => 'index' and return unless params[:order] && params[:items]
+
+ @order = Order.find_by_uuid(params[:order][:uuid])
+
+ if @order
+ if @order.status == 'C'
+ session[:order_id] = @order.id
+ redirect_to :action => 'thankyou'
+ elsif @order.status == 'S'
+ # If the order was already submitted, wait half a second and call purchase again.
+ # Hopefully it'll complete and we can be redirected to the thank you page
+ sleep 0.5
+ self.purchase
+ else
+ render :action => 'failed', :layout => 'error'
+ end
+ return
+ end
+
+ # We need the next two ugly lines because Safari's form autofill sucks
+ params[:order][:address1] = params[:address1]
+ params[:order][:address2] = params[:address2]
+
+ params[:order].keys.each { |x| params[:order][x] = params[:order][x].strip if params[:order][x] != nil }
+
+ @order = Order.new(params[:order])
+
+ # the order in the session is a bogus temporary one
+ @order.add_form_items(params[:items])
+
+ if params[:coupon]
+ @order.coupon_text = params[:coupon]
+ end
+
+ @order.order_time = Time.now()
+ @order.status = 'S'
+
+ if not @order.save()
+ flash[:error] = 'Please fill out all fields'
+ if @order.cc_order?
+ render :action => 'payment_cc' and return
+ else
+ render :action => 'payment_gcheckout' and return
+ end
+ end
+
+ # Do this after saving order
+ session[:order_id] = @order.id
+ session[:items] = nil
+
+ # Actually send out the payload
+ if @order.cc_order?
+ success = @order.paypal_direct_payment(request)
+ finish_order(success)
+ else
+ # Google Checkout order
+ redirect_url = @order.gcheckout_send_order(url_for(:action => 'index'))
+ if redirect_url == nil
+ @order.failure_reason = 'Could not connect to Google Checkout'
+ render :action => 'failed', :layout => 'error' and return
+ end
+ redirect_to redirect_url and return
+ end
+ end
+
+ def confirm_paypal
+ render :action => 'no_order', :layout => 'error' and return if session[:order_id] == nil
+
+ @order = Order.find(session[:order_id])
+ redirect_to :action => 'index' and return if @order == nil || session[:paypal_token] != params[:token]
+
+ # Suck the info from PayPal
+ if not @order.update_from_paypal_express_checkout_details(session[:paypal_token])
+ flash[:notice] = 'Could not retrieve order information from PayPal'
+ redirect_to :action => 'index' and return
+ end
+
+ session[:paypal_payer_id] = params['PayerID']
+ @order.payment_type = 'PayPal'
+
+ if not @order.save()
+ flash[:error] = 'Problem saving order'
+ render :action => 'confirm_paypal' and return
+ end
+
+ session[:order_id] = @order.id
+ end
+
+ def purchase_paypal
+ render :action => 'no_order', :layout => 'error' and return if session[:order_id] == nil
+
+ @order = Order.find(session[:order_id])
+ @order.attributes = params[:order]
+
+ redirect_to :action => 'index' and return if session[:paypal_token] == nil
+ render :action => 'failed', :layout => 'error' and return if !@order.pending?
+
+ @order.order_time = Time.now()
+ @order.status = 'S'
+
+ if not @order.save()
+ flash[:error] = 'Please fill out all fields'
+ render :action => 'confirm_paypal' and return
+ end
+
+ success = @order.paypal_express_checkout_payment(session[:paypal_token], session[:paypal_payer_id])
+
+ finish_order(success)
+ end
+
+ ## Methods that need a completed order
+ before_filter :check_completed_order, :only => [:thankyou, :receipt]
+
+ def thankyou
+ # no need to check for nil order in the session here.
+ # check_completed_order is a before_filter for this method
+ @order = Order.find(session[:order_id])
+ end
+
+ def receipt
+ # no need to check for nil order in the session here.
+ # check_completed_order is a before_filter for this method
+ @order = Order.find(session[:order_id])
+ @print = true
+ render :partial => 'receipt'
+ end
+
+ ## Private methods
+ private
+ def check_completed_order
+ @order = Order.find(session[:order_id])
+ unless @order && @order.complete?
+ redirect_to :action => "index"
+ end
+ end
+
+ private
+ def finish_order(success)
+ if params[:subscribe] && params[:subscribe] == 'checked'
+ @order.subscribe_to_list()
+ end
+
+ @order.status = success ? 'C' : 'F'
+ @order.finish_and_save()
+
+ if success
+ session[:order_id] = @order.id
+ redirect_to :action => 'thankyou'
+ else
+ render :action => 'failed', :layout => 'error'
+ end
+ end
+
+end
diff --git a/app/controllers/store/products_controller.rb b/app/controllers/store/products_controller.rb
new file mode 100644
index 0000000..3dcc7d0
--- /dev/null
+++ b/app/controllers/store/products_controller.rb
@@ -0,0 +1,17 @@
+class Store::ProductsController < ApplicationController
+ layout "store"
+
+ # GET /products
+ # GET /products.xml
+ # GET /products.json
+ def index
+ @products = Product.find(:all, :conditions => {:active => 1 })
+
+ respond_to do |format|
+ format.html # index.rhtml
+ format.json { render :json => @products.to_json }
+ format.xml { render :xml => @products.to_xml }
+ end
+ end
+
+end
diff --git a/app/helpers/admin/charts_helper.rb b/app/helpers/admin/charts_helper.rb
new file mode 100644
index 0000000..271b636
--- /dev/null
+++ b/app/helpers/admin/charts_helper.rb
@@ -0,0 +1,2 @@
+module Admin::ChartsHelper
+end
diff --git a/app/helpers/admin/coupons_helper.rb b/app/helpers/admin/coupons_helper.rb
new file mode 100644
index 0000000..ce6fadb
--- /dev/null
+++ b/app/helpers/admin/coupons_helper.rb
@@ -0,0 +1,2 @@
+module Admin::CouponsHelper
+end
diff --git a/app/helpers/admin/orders_helper.rb b/app/helpers/admin/orders_helper.rb
new file mode 100644
index 0000000..863374f
--- /dev/null
+++ b/app/helpers/admin/orders_helper.rb
@@ -0,0 +1,2 @@
+module Admin::OrdersHelper
+end
diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb
new file mode 100644
index 0000000..977a242
--- /dev/null
+++ b/app/helpers/admin/products_helper.rb
@@ -0,0 +1,2 @@
+module Admin::ProductsHelper
+end
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
new file mode 100644
index 0000000..d5c6d35
--- /dev/null
+++ b/app/helpers/admin_helper.rb
@@ -0,0 +1,2 @@
+module AdminHelper
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
new file mode 100644
index 0000000..1e234c3
--- /dev/null
+++ b/app/helpers/application_helper.rb
@@ -0,0 +1,25 @@
+# Methods added to this helper will be available to all templates in the application.
+def load_country_names()
+ app_root = File.dirname(__FILE__) + '/../..'
+ config_dir = app_root + '/config/'
+ ymlpath = File.expand_path(config_dir + 'countries.yml')
+ $COUNTRY_NAMES = YAML.load(File.open(ymlpath))
+end
+
+load_country_names()
+
+# I know it's not good to make this method global, but I can't get the thankyou_plain.rhtml template to call it otherwise
+def country_name(country_code)
+ return $COUNTRY_NAMES[country_code]
+end
+
+module ApplicationHelper
+ def country_name_and_code_pairs()
+ pairs = []
+ for pair in $COUNTRY_NAMES
+ pairs << [pair[1], pair[0]]
+ end
+ pairs.sort! {|x,y| x[0] <=> y[0]}
+ return pairs
+ end
+end
diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb
new file mode 100644
index 0000000..37a644c
--- /dev/null
+++ b/app/helpers/email_helper.rb
@@ -0,0 +1,2 @@
+module EmailHelper
+end
diff --git a/app/helpers/store/lost_license_helper.rb b/app/helpers/store/lost_license_helper.rb
new file mode 100644
index 0000000..bb853ba
--- /dev/null
+++ b/app/helpers/store/lost_license_helper.rb
@@ -0,0 +1,2 @@
+module Store::LostLicenseHelper
+end
diff --git a/app/helpers/store/notification_helper.rb b/app/helpers/store/notification_helper.rb
new file mode 100644
index 0000000..f98641d
--- /dev/null
+++ b/app/helpers/store/notification_helper.rb
@@ -0,0 +1,2 @@
+module Store::NotificationHelper
+end
diff --git a/app/helpers/store/order_helper.rb b/app/helpers/store/order_helper.rb
new file mode 100644
index 0000000..9549c3a
--- /dev/null
+++ b/app/helpers/store/order_helper.rb
@@ -0,0 +1,2 @@
+module Store::OrderHelper
+end
diff --git a/app/models/admin.rb b/app/models/admin.rb
new file mode 100644
index 0000000..a38e3f4
--- /dev/null
+++ b/app/models/admin.rb
@@ -0,0 +1,5 @@
+module Admin
+ def self.table_name_prefix
+ 'admin_'
+ end
+end
diff --git a/app/models/coupon.rb b/app/models/coupon.rb
new file mode 100644
index 0000000..135a618
--- /dev/null
+++ b/app/models/coupon.rb
@@ -0,0 +1,24 @@
+class Coupon < ActiveRecord::Base
+ validate :code, :presence => true
+
+ def initialize
+ super()
+ self.used_count = 0
+ self.use_limit = 1
+ end
+
+ def expired?
+ (self.used_count >= self.use_limit) ||
+ (self.numdays != 0 && self.creation_time + self.numdays.days < Time.now) ||
+ (self.expiration_date != nil && self.expiration_date < Time.now)
+ end
+
+ private
+ def self.random_string_of_length(len)
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
+ s = ""
+ 1.upto(len) { |i| s << chars[rand(chars.size-1)] }
+ return s
+ end
+
+end
diff --git a/app/models/line_item.rb b/app/models/line_item.rb
new file mode 100644
index 0000000..b0368d1
--- /dev/null
+++ b/app/models/line_item.rb
@@ -0,0 +1,51 @@
+require 'licensekey'
+
+class LineItem < ActiveRecord::Base
+ belongs_to :order
+ belongs_to :product
+
+ validates_numericality_of :quantity, :only_integer => true
+ validates_numericality_of :unit_price
+
+ attr_protected :license_key
+
+ def quantity=(qty)
+ # take care of leading zeroes so that the quantity does not get treated as an octal number
+ qty = qty.to_s.strip.sub(/^0+/, '')
+ qty = 0 if qty == ''
+ qty = Integer(Float(qty).ceil)
+
+ # Don't automatically generate a new key for pending orders
+ regenerate_keys = (self.quantity != qty) && self.order.status != 'P'
+ write_attribute(:quantity, qty)
+
+ if regenerate_keys
+ self.license_key = make_license(self.product.code, self.order.licensee_name, qty)
+ end
+ end
+
+ def total
+ return round_money(quantity * self.unit_price)
+ end
+
+ def volume_price
+ # This is hard coded for now. Modify to suit your needs
+ if self.product.code == 'pcm'
+ return 19.95 if quantity >= 20
+ return 22.95 if quantity >= 10
+ return 24.95 if quantity >= 2
+ elsif self.product.code == 'vc'
+ return 9.95 if self.order.has_item_with_code('tgr')
+ end
+ return unit_price
+ end
+
+ def regular_price
+ return product.price
+ end
+
+ def license_url
+ return self.product.license_url_scheme + '://' + self.license_key rescue nil
+ end
+
+end
diff --git a/app/models/list_subscriber.rb b/app/models/list_subscriber.rb
new file mode 100644
index 0000000..8b49a43
--- /dev/null
+++ b/app/models/list_subscriber.rb
@@ -0,0 +1,2 @@
+class ListSubscriber < ActiveRecord::Base
+end
diff --git a/app/models/order.rb b/app/models/order.rb
new file mode 100644
index 0000000..fb948ef
--- /dev/null
+++ b/app/models/order.rb
@@ -0,0 +1,685 @@
+require 'uuidtools'
+require 'ruby-paypal'
+
+class Order < ActiveRecord::Base
+ has_many :line_items
+ belongs_to :coupon
+ before_create :generate_token
+
+ attr_accessor :cc_code, :cc_month, :cc_year
+ attr_accessor :skip_cc_validation
+ attr_accessor :email_receipt_when_finishing
+ attr_writer :promo_coupons
+
+ attr_protected :status, :skip_cc_validation
+
+ validates_presence_of :payment_type
+
+ def initialize(attributes = {}, form_items = [])
+ attributes = attributes.clone()
+ if attributes[:items]
+ form_items = attributes[:items]
+ attributes.delete(:items)
+ end
+
+ super(attributes)
+
+ if form_items.length > 0
+ self.add_form_items(form_items)
+ end
+
+ self.order_time = Time.now() if not self.order_time
+ end
+
+ def validate
+ ## Validate credit card order
+ if self.cc_order? && !skip_cc_validation
+ errors.add_on_blank(['first_name', 'last_name', 'address1', 'city', 'country', 'email'])
+ if self.pending? or self.submitting?
+ errors.add_on_blank(['cc_number', 'cc_month', 'cc_year', 'cc_code'])
+ end
+
+ if ['US', 'CA', 'JP'].member?(self.country)
+ errors.add_on_blank('zipcode')
+ end
+
+ if ['US', 'CA'].member?(self.country)
+ errors.add('state', msg = 'must be a 2 character abbreviation for USA and Canada') if self.state.size != 2
+ end
+ end
+
+ ## Validate PayPal order
+ if self.paypal_order?
+ if self.submitting? or self.complete?
+ errors.add_on_blank(['email'])
+ end
+ end
+
+ ## If licensee name is all alpha-numeric, require it to be at least 8 characters.
+ if self.submitting? or self.complete?
+ if self.licensee_name == nil || self.licensee_name.strip == ''
+ errors.add_on_blank(['licensee_name'])
+ elsif (self.licensee_name =~ /^[\w ]*$/) != nil && self.licensee_name.length < 8
+ errors.add('licensee_name', msg= 'must be at least 8 characters long')
+ end
+ end
+ end
+
+ def calculated_total
+ return round_money(total_before_applying_coupons() - coupon_amount())
+ end
+
+ def total_before_applying_coupons
+ total = 0
+ for item in self.line_items
+ total = total + item.total
+ end
+ return round_money(total)
+ end
+
+ ## tax and shipping are hard-wired to 0 for now
+ def tax_total
+ return 0
+ end
+
+ def shipping_total
+ return 0
+ end
+
+ def coupon_amount
+ return 0 if coupon == nil
+ return coupon.amount if coupon.percentage == nil
+ for item in self.line_items
+ if coupon && coupon.percentage != nil && coupon.product_code == item.product.code
+ return round_money(item.total * coupon.percentage / 100.0)
+ end
+ end
+ if coupon && coupon.percentage != nil && coupon.product_code == 'all'
+ return round_money(total_before_applying_coupons() * (coupon.percentage / 100.0))
+ end
+ return 0
+ end
+
+ def volume_discount_total
+ total = self.total()
+ self.line_items.collect{|x| x.regular_price * x.quantity}.each {|x| total -= x}
+ return -total
+ end
+
+ def items_count
+ return self.line_items.reject{|x| x.quantity <= 0}.length
+ end
+
+ def product_quantity(product_code)
+ for item in self.line_items
+ return item.quantity if item.product.code == product_code
+ end
+ return 0
+ end
+
+ def licensee_name=(new_name)
+ regenerate_keys = (self.licensee_name != new_name) && (self.submitting? or self.complete?)
+ write_attribute(:licensee_name, new_name.strip())
+ if regenerate_keys
+ for item in self.line_items
+ item.license_key = make_license(item.product.code, new_name, item.quantity)
+ end
+ end
+ end
+
+ def first_name=(value)
+ write_attribute(:first_name, value.strip())
+ end
+
+ def last_name=(value)
+ write_attribute(:last_name, value.strip())
+ end
+
+ def name
+ return self.first_name + ' ' + self.last_name
+ end
+
+ def address
+ address = self.address1
+ address += ', ' + self.address2 if not self.address2.blank?
+ return address
+ end
+
+ def cc_number
+ return @cc_number
+ end
+
+ def cc_number=(num)
+ t = num.tr('- ', '')
+ @cc_number = t
+ return if t.length < 4
+ self.ccnum = 'X' * (t.length - 4) + t[t.length-4 .. t.length-1]
+ end
+
+ def payment_type=(type)
+ val = type
+ if type
+ if ['visa', 'amex', 'discover'].member?(type.downcase())
+ val = type.capitalize()
+ elsif type.downcase() == 'mastercard'
+ val = 'MasterCard'
+ end
+ end
+ write_attribute(:payment_type, val)
+ end
+
+ def cc_order?
+ return self.payment_type != nil && ['visa', 'mastercard', 'amex', 'discover'].member?(self.payment_type.downcase)
+ end
+
+ def paypal_order?
+ return self.payment_type != nil && self.payment_type.downcase == 'paypal'
+ end
+
+ def gcheckout?
+ return self.payment_type != nil && self.payment_type.downcase == 'google checkout'
+ end
+
+ def pending?
+ return self.status == 'P'
+ end
+
+ def complete?
+ return self.status == 'C'
+ end
+
+ def submitting?
+ return self.status == 'S'
+ end
+
+ def status=(new_status)
+ if (self.pending? or self.submitting?) && new_status == 'C'
+ self.email_receipt_when_finishing = true
+ end
+ write_attribute(:status, new_status)
+ end
+
+ def status_description
+ case self.status
+ when 'C'
+ return 'Complete'
+ when 'P'
+ return 'Pending'
+ when 'S'
+ return 'Submitting'
+ when 'F'
+ return 'Failed'
+ when 'X'
+ return 'Cancelled'
+ when 'R'
+ return 'Refunded'
+ end
+ return self.status
+ end
+
+ def coupon_text
+ self.coupon.coupon
+ end
+
+ def coupon_text=(coupon_text)
+ return if !coupon_text || coupon_text.strip == ''
+ coupon = Coupon.find_by_coupon(coupon_text.strip)
+ if coupon != nil && self.coupon == nil &&
+ (coupon.product_code == 'all' || has_item_with_code(coupon.product_code)) &&
+ coupon.enabled? && !coupon.expired?
+ self.coupon = coupon
+ self.total = self.calculated_total
+ end
+ end
+
+ def has_item_with_product_id(product_id)
+ return self.line_items.collect {|x| x.product.id if x.quantity > 0}.member?(product_id)
+ end
+
+ def has_item_with_code(code)
+ return self.line_items.collect {|x| x.product.code if x.quantity > 0}.member?(code)
+ end
+
+ def line_item_with_product_id(product_id)
+ begin
+ product_id = Integer(product_id)
+ rescue
+ return nil
+ end
+ items = self.line_items.collect {|x| x if x.product.id == product_id}.compact
+ return nil if items.length == 0
+ return items[0]
+ end
+
+ def line_item_with_product_code(product_code)
+ p = Product.find_by_code(product_code)
+ return self.line_item_with_product_id(p.id)
+ end
+
+ # Add items from form parameters
+ def add_form_items(items)
+ begin
+ for product_id in items.keys
+ next if items[product_id].to_s.strip == ''
+ item = LineItem.new({:order => self,:product_id => product_id})
+ item.quantity = items[product_id].to_i
+ next if item.quantity == 0
+ return false if item.quantity < 0
+
+ item.unit_price = Product.find(product_id).price
+ self.line_items << item
+ end
+ for item in self.line_items
+ item.unit_price = item.volume_price
+ end
+ self.total = self.calculated_total
+ return true
+ rescue
+ logger.error("Could not add form product items: #{$!}")
+ return false
+ end
+ end
+
+ def add_or_update_items(items)
+ for product_id in items.keys
+ next if items[product_id].to_s.strip == ''
+ litem = self.line_item_with_product_id(product_id)
+ if litem == nil
+ return false if not self.add_form_items({product_id => items[product_id]})
+ else
+ quantity = items[product_id].to_i
+ if quantity <= 0
+ litem.destroy()
+ self.line_items.delete(litem)
+ else
+ litem.quantity = quantity
+ end
+ end
+ end
+ return true
+ end
+
+ # Updates the prices from form
+ def update_item_prices(item_prices)
+ for product_id in item_prices.keys
+ litem = self.line_item_with_product_id(product_id)
+ next if litem == nil
+ litem.unit_price = item_prices[product_id]
+ end
+ end
+
+ def promo_coupons
+ return @promo_coupons if @promo_coupons
+ return []
+ end
+
+ # Create new coupons that pertain to this order and return it
+ def add_promo_coupons
+ self.promo_coupons = []
+ # if the order contains Voice Candy, create 3 coupons
+# if self.has_item_with_code('vc')
+# 1.upto(3) { |i|
+# coupon = Coupon.new
+# coupon.code = 'vc'
+# coupon.product_code = 'vc'
+# coupon.description = 'Cool friend discount'
+# coupon.amount = 3.00
+# coupon.numdays = 16
+# coupon.save()
+# coupons << coupon
+# }
+# end
+ return promo_coupons
+ end
+
+ def subscribe_to_list
+ return if ListSubscriber.find_by_email(self.email) != nil
+ subscriber = ListSubscriber.new
+ subscriber.email = self.email
+ subscriber.save()
+ end
+
+ def save
+ # Insert a dash for Japanese zipcodes if it doesn't have one
+ if self.country == 'JP' && self.zipcode != nil && self.zipcode.count('-') == 0 && self.zipcode.length > 3
+ self.zipcode.insert(3, '-')
+ end
+
+ # Take out optional strings
+ self.address2 = '' if self.address2 != nil && self.address2.strip == 'optional'
+ self.company = '' if self.company != nil && self.company.strip == 'optional'
+ self.comment = '' if self.comment != nil && self.comment.strip == 'optional'
+
+ # Save updated relationships
+ for item in self.line_items
+ if !item.new_record? && item.changed?
+ item.save()
+ end
+ end
+
+ if self.coupon != nil && !self.coupon.new_record? && self.coupon.changed?
+ self.coupon.save()
+ end
+
+ # Add UID if it hasn't been already
+ self.uuid = generate_token() unless self.uuid
+
+ # Always update the total before saving. Always!!!
+ self.total = self.calculated_total
+
+ super()
+ end
+
+ def finish_and_save
+ if self.status == 'C'
+ self.add_promo_coupons()
+ for line_item in self.line_items
+ if line_item.license_key.nil? then
+ line_item.license_key = make_license(line_item.product.code, self.licensee_name, line_item.quantity)
+ end
+ end
+ if self.coupon
+ self.coupon.used_count += 1
+ end
+ end
+
+ self.save()
+
+ if self.email_receipt_when_finishing && !self.gcheckout?
+ # Google Checkout orders get the emails delivered when the final OK notification from Google arrives
+ OrderMailer.thankyou(self).deliver if is_live?()
+ end
+ end
+
+ def refund
+ return if self.status != 'C' and self.status != 'X'
+
+ if cc_order? or paypal_order?
+ self.paypal_refund_order()
+ else
+ self.gcheckout_refund_order()
+ end
+ end
+
+ # PayPal related methods
+ def paypal_direct_payment(request)
+ # The following is needed because MediaTemple puts in two ip addresses in REMOTE_ADDR for some reason
+ ip_address = request.env['REMOTE_ADDR']
+ ip_address = ip_address.split(',')[0] if ip_address.count(",") != 0
+ ip_address = "127.0.0.1" if ip_address == "::1"
+
+ cc_month = self.cc_month.to_s
+ cc_month = '0' + cc_month if cc_month.length == 1
+
+ cc_year = self.cc_year.to_s
+ cc_year = '20' + cc_year if cc_year.length == 2
+
+ params = {
+ 'method' => 'DoDirectPayment',
+ 'ipaddress' => ip_address,
+ 'creditcardtype' => self.payment_type,
+ 'acct' => self.cc_number,
+ 'expdate' => cc_month + cc_year,
+ 'cvv2' => self.cc_code,
+ 'firstname' => self.first_name,
+ 'lastname' => self.last_name,
+ 'street' => self.address1,
+ 'street2' => self.address2,
+ 'city' => self.city,
+ 'state' => (self.state.blank?) ? 'N/A' : self.state,
+ 'countrycode' => self.country,
+ 'zip' => self.zipcode,
+ 'amt' => round_money(self.total).to_f,
+ 'invnum' => self.uuid.gsub('-', '')
+ }
+
+ self.line_items.each_with_index do |item, i|
+ params["l_number#{i}"] = item.product.code
+ params["l_name#{i}"] = item.product.name
+ params["l_amt#{i}"] = round_money(item.unit_price).to_f
+ params["l_qty#{i}"] = item.quantity
+ end
+
+ if self.coupon
+ params["l_number#{self.line_items.count}"] = self.line_items.count
+ params["l_name#{self.line_items.count}"] = self.coupon.description
+ params["l_amt#{self.line_items.count}"] = 0 - self.coupon.amount
+ params["l_qty#{self.line_items.count}"] = 1
+ end
+
+
+ res = PayPal.make_nvp_call(params)
+
+ if res.ack == 'Success' || res.ack == 'SuccessWithWarning'
+ self.transaction_number = res.transactionID
+ return true
+ else
+ set_order_errors_with_paypal_response(res)
+ return false
+ end
+ end
+
+ def paypal_set_express_checkout(return_url, cancel_url)
+ params = {
+ 'method' => 'SetExpressCheckout',
+ 'returnURL' => return_url,
+ 'cancelURL' => cancel_url,
+ 'paymentrequest_0_amt' => round_money(self.total).to_f,
+ 'noshipping' => 1,
+ 'allownote' => 0,
+ 'channeltype' => 'Merchant',
+ 'hdrimg' => $STORE_PREFS['paypal_express_checkout_header_image']
+ }
+
+ self.line_items.each_with_index do |item, i|
+ params["l_paymentrequest_0_number#{i}"] = item.product.code
+ params["l_paymentrequest_0_name#{i}"] = item.product.name
+ params["l_paymentrequest_0_amt#{i}"] = item.unit_price
+ params["l_paymentrequest_0_qty#{i}"] = item.quantity
+ end
+
+ if self.coupon
+ params["l_paymentrequest_0_number#{self.line_items.count + 1}"] = self.line_items.count
+ params["l_paymentrequest_0_name#{self.line_items.count + 1}"] = self.coupon.description
+ params["l_paymentrequest_0_amt#{self.line_items.count + 1}"] = 0 - self.coupon.amount
+ params["l_paymentrequest_0_qty#{self.line_items.count + 1}"] = 1
+ end
+
+ return PayPal.make_nvp_call(params)
+ end
+
+ def paypal_express_checkout_payment(token, payer_id)
+ params = {
+ 'method' => 'DoExpressCheckoutPayment',
+ 'token' => token,
+ 'payerID' => payer_id,
+ 'paymentrequest_0_paymentaction' => 'Sale',
+ 'paymentrequest_0_invnum' => self.uuid.gsub('-', ''),
+ 'paymentrequest_0_amt' => round_money(self.total).to_f
+ }
+
+ self.line_items.each_with_index do |item, i|
+ params["l_paymentrequest_0_number#{i}"] = item.product.code
+ params["l_paymentrequest_0_name#{i}"] = item.product.name
+ params["l_paymentrequest_0_amt#{i}"] = item.unit_price
+ params["l_paymentrequest_0_qty#{i}"] = item.quantity
+ end
+
+ if self.coupon
+ params["l_paymentrequest_0_number#{self.line_items.count}"] = self.line_items.count
+ params["l_paymentrequest_0_name#{self.line_items.count}"] = self.coupon.description
+ params["l_paymentrequest_0_amt#{self.line_items.count}"] = 0 - self.coupon.amount
+ params["l_paymentrequest_0_qty#{self.line_items.count}"] = 1
+ end
+
+ res = PayPal.make_nvp_call(params)
+
+ if res.ack == 'Success' || res.ack == 'SuccessWithWarning'
+ self.transaction_number = res.paymentInfo_0_transactionID
+ return true
+ else
+ set_order_errors_with_paypal_response(res)
+ return false
+ end
+ end
+
+ def paypal_refund_order
+ itemsdesc = self.line_items.collect {|x| x.product.name}.to_sentence
+ company = $STORE_PREFS['company_name']
+
+ params = {
+ 'method' => 'RefundTransaction',
+ 'transactionID' => self.transaction_number,
+ 'invoiceID' => self.id,
+ 'note' => "Refund for #{itemsdesc} from #{company}"
+ }
+
+ res = PayPal.make_nvp_call(params)
+
+ if res.ack == 'Success' || res.ack == 'SuccessWithWarning'
+ self.status = 'R'
+ self.save()
+ end
+ end
+
+ def update_from_paypal_express_checkout_details(token)
+ res = PayPal.make_nvp_call('method' => 'GetExpressCheckoutDetails', 'token' => token)
+ if res.ack == 'Success' || res.ack == 'SuccessWithWarning'
+ self.first_name = res.firstname
+ self.last_name = res.lastname
+ self.email = res.email
+ self.country = res.countrycode
+ self.licensee_name = self.first_name + " " + self.last_name
+ return true
+ else
+ return false
+ end
+ end
+
+ def set_order_errors_with_paypal_response(res)
+ if res.has_key? 'PAYMENTREQUEST_0_ACK'
+ ack = res['PAYMENTREQUEST_0_ACK']
+
+ if ack.start_with? 'Failure'
+ self.failure_code = res['PAYMENTREQUEST_0_ERRORCODE']
+ self.failure_reason = res['PAYMENTREQUEST_0_LONGMESSAGE']
+ end
+ else
+ ack = res['ACK']
+
+ if ack and ack.start_with? 'Failure'
+ i = 0
+ error_codes = []
+ failure_reasons = []
+
+ while true
+ break if not res.has_key? "L_ERRORCODE#{i}"
+ error_codes << res["L_ERRORCODE#{i}"]
+
+ msg = res["L_LONGMESSAGE#{i}"]
+ msg = res["L_SHORTMESSAGE#{i}"] if not msg
+ msg = '' if not msg
+ msg = msg[38..-1] if msg =~ /^This transaction cannot be processed. /
+
+ failure_reasons << msg if msg
+
+ i += 1
+ end
+
+ self.failure_code = error_codes.join(', ')
+ self.failure_reason = failure_reasons.join("\n")
+ end
+ end
+ end
+
+ # Google Checkout related methods
+ def gcheckout_send_order(edit_cart_url = nil)
+ command = $GCHECKOUT_FRONTEND.create_checkout_command
+ command.continue_shopping_url = $STORE_PREFS['company_url']
+ command.edit_cart_url = edit_cart_url
+
+ for line_item in self.line_items
+ command.shopping_cart.create_item do |item|
+ item.name = line_item.product.name
+ item.description = ""
+ item.unit_price = Money.new(line_item.unit_price * 100)
+ item.quantity = line_item.quantity
+
+ # Force a license key generation here even though the order status is still P.
+ # The order doesn't become status C until we get final notification from Google,
+ # but we're still optimistically showing the buyer their license key because otherwise
+ # the delay can be as much as 20 minutes on the GCheckout side
+ item.create_digital_content do |dc|
+ dc.display_disposition = Google4R::Checkout::Item::DigitalContent::OPTIMISTIC
+ dc.description = "#{line_item.product.name}, licensed to #{self.licensee_name}"
+ dc.key = make_license(line_item.product.code, self.licensee_name, line_item.quantity)
+ end
+ end
+ end
+
+ if self.coupon
+ command.shopping_cart.create_item do |item|
+ item.name = "Coupon"
+ item.description = coupon.description
+ item.unit_price = Money.new(-coupon_amount() * 100)
+ item.quantity = 1
+ end
+ end
+
+ command.shopping_cart.private_data = { 'order-id' => [self.id] }
+
+ begin
+ res = command.send_to_google_checkout()
+ if self.coupon
+ self.coupon.used_count += 1
+ self.save
+ end
+ return res.redirect_url
+ rescue
+ logger.error("An error while talking to google checkout: #{$!}")
+ return nil
+ end
+ end
+
+ def gcheckout_add_merchant_order_number
+ command = $GCHECKOUT_FRONTEND.create_add_merchant_order_number_command
+ command.google_order_number = self.transaction_number
+ command.merchant_order_number = self.id
+ command.send_to_google_checkout() rescue nil
+ end
+
+ def gcheckout_deliver_order
+ command = $GCHECKOUT_FRONTEND.create_deliver_order_command
+ command.google_order_number = self.transaction_number
+ command.send_email = false
+ command.send_to_google_checkout() rescue nil
+ end
+
+ def gcheckout_archive_order
+ command = $GCHECKOUT_FRONTEND.create_archive_order_command
+ command.google_order_number = self.transaction_number
+ command.send_to_google_checkout() rescue nil
+ end
+
+ def gcheckout_refund_order
+ command = $GCHECKOUT_FRONTEND.create_refund_order_command
+ command.google_order_number = self.transaction_number
+ command.reason = 'Refund requested by customer'
+ command.send_to_google_checkout() rescue nil
+
+ command = $GCHECKOUT_FRONTEND.create_cancel_order_command
+ command.google_order_number = self.transaction_number
+ command.reason = 'Refund requested by customer'
+ command.send_to_google_checkout() rescue nil
+
+ self.status = 'R'
+ self.save()
+ end
+
+ private
+ def generate_token
+ token = UUIDTools::UUID.timestamp_create.to_s
+ self.uuid = token
+ return token
+ end
+end
diff --git a/app/models/order_mailer.rb b/app/models/order_mailer.rb
new file mode 100644
index 0000000..88acaa8
--- /dev/null
+++ b/app/models/order_mailer.rb
@@ -0,0 +1,27 @@
+class OrderMailer < ActionMailer::Base
+
+ def thankyou(order, bcc = true)
+ @order = order;
+ mail(
+ :to => order.email,
+ :subject => "Purchase Receipt for Order ##{order.id}",
+ :from => $STORE_PREFS['purchase_receipt_sender_email'],
+ :bcc => bcc ? $STORE_PREFS['purchase_receipt_bcc_email'] : ""
+ ) do |format|
+ format.text # comment out either one to test
+ format.html
+ end
+ end
+
+ def lost_license_sent(order)
+ @order = order;
+ mail(
+ :to => $STORE_PREFS['lost_license_sent_notification_recipient_email'],
+ :subject => 'Lost License Sent',
+ :from => $STORE_PREFS['purchase_receipt_sender_email']
+ ) do |format|
+ format.text
+ end
+ end
+
+end
diff --git a/app/models/product.rb b/app/models/product.rb
new file mode 100644
index 0000000..077a819
--- /dev/null
+++ b/app/models/product.rb
@@ -0,0 +1,2 @@
+class Product < ActiveRecord::Base
+end
diff --git a/app/models/support_mailer.rb b/app/models/support_mailer.rb
new file mode 100644
index 0000000..52ecea8
--- /dev/null
+++ b/app/models/support_mailer.rb
@@ -0,0 +1,49 @@
+class SupportMailer < ActionMailer::Base
+
+ def support_request(fields)
+ subject "#{fields["product"]} Support"
+ subject "#{self.subject}: #{fields["subject"]}" if not fields["subject"].blank?
+
+ # add a star to the subject to mark that the email is from a registered customer
+ subject "#{self.subject} *" if fields["registered"] == "1"
+
+ recipients fields["recipient"]
+ from fields["replyTo"]
+
+ email_body = fields["message"] + "\r\n\r\n"
+
+ exclude_fields = ["product", "recipient", "replyTo", "subject", "message", "action", "controller"]
+
+ fields.each do |key, value|
+ if fields[key].respond_to?("original_filename") == false && exclude_fields.include?(key) == false && fields[key] != nil
+ email_body = email_body + fields[key]
+ end
+ end
+ email_body = email_body + "\r\n\r\n"
+
+ body = {}
+ part :content_type => "text/plain", :body => email_body
+
+ fields.each do |key, value|
+ if fields[key].respond_to?("original_filename") == true && exclude_fields.include?(key) == false && fields[key] != nil
+ attachment "application/binary" do |x|
+ x.filename = fields[key].original_filename.gsub(/[^a-zA-Z0-9.]/, '_')
+ x.body = fields[key].read
+ end
+ end
+ end
+ end
+
+ def crash_report(product, report)
+ if is_live?()
+ subject "#{product} Crash Report"
+ else
+ subject "#{product} Crash Report (this is a test)"
+ end
+ recipients $STORE_PREFS['crash_report_recipient_email']
+ from $STORE_PREFS['crash_report_sender_email']
+
+ body :report => report
+ end
+
+end
diff --git a/app/views/admin/_revenue_history.html.erb b/app/views/admin/_revenue_history.html.erb
new file mode 100644
index 0000000..4d5bb85
--- /dev/null
+++ b/app/views/admin/_revenue_history.html.erb
@@ -0,0 +1,3 @@
+<%= @chart.html_safe %>
+
+
diff --git a/app/views/admin/_revenue_summary.html.erb b/app/views/admin/_revenue_summary.html.erb
new file mode 100644
index 0000000..18b84e9
--- /dev/null
+++ b/app/views/admin/_revenue_summary.html.erb
@@ -0,0 +1,42 @@
+
+
+
+ Today
+ 7 Days
+ 30 Days
+ 365 Days
+
+
+ Orders
+ <% for val in @num_orders %>
+ <%= val %>
+ <% end %>
+
+ <% for product in @products %>
+
+ <%= product %>
+ <% for i in 0..3 %>
+ <% if @type == nil || @type == "quantity" %>
+ <%= @product_quantity[product][i] %>
+ <% elsif @type == "amount" %>
+ <%= number_to_currency(@product_revenue[product][i]) %>
+ <% elsif @type == "percentage" %>
+ <%= number_to_percentage @product_percentage[product][i], :precision => 0 %>
+ <% end %>
+ <% end %>
+
+ <% end %>
+
+ Total
+ <% for val in @revenue %>
+ <%= number_to_currency(val) %>
+ <% end %>
+
+
+
+
+ Projections — This month: <%= number_to_currency(@month_estimate, :precision => 0) %> /
+ Next 365 Days: <%= number_to_currency(@year_estimate, :precision => 0) %>
+
+
+
diff --git a/app/views/admin/add_coupons.html.erb b/app/views/admin/add_coupons.html.erb
new file mode 100644
index 0000000..08590db
--- /dev/null
+++ b/app/views/admin/add_coupons.html.erb
@@ -0,0 +1,8 @@
+<% @qty = {} %>
+<%= form_tag :action => "add_coupons" do %>
+Code: <%= text_field 'form', 'code' %>
+Amount: <%= text_field 'form', 'amount' %>
+Description: <%= text_field 'form', 'description' %>
+Coupons<%= text_area 'form', 'coupons' %>
+<%= submit_tag 'Process Orders' %>
+<% end %>
diff --git a/app/views/admin/coupons/_form.html.erb b/app/views/admin/coupons/_form.html.erb
new file mode 100644
index 0000000..992238e
--- /dev/null
+++ b/app/views/admin/coupons/_form.html.erb
@@ -0,0 +1,66 @@
+<%= form_for [:admin, @coupon] do |f| %>
+ <% if @coupon.errors.any? %>
+
+
<%= pluralize(@coupon.errors.count, "error") %> prohibited this admin_coupon from being saved:
+
+
+ <% @coupon.errors.full_messages.each do |msg| %>
+ <%= msg %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+ <%= f.submit %>
+
+<% end %>
+
+
+
diff --git a/app/views/admin/coupons/edit.html.erb b/app/views/admin/coupons/edit.html.erb
new file mode 100644
index 0000000..b40a9c4
--- /dev/null
+++ b/app/views/admin/coupons/edit.html.erb
@@ -0,0 +1,5 @@
+Editing Coupon - <%= @coupon.code %> #<%= @coupon.id %>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', admin_coupons_path(@coupon) %>
diff --git a/app/views/admin/coupons/index.html.erb b/app/views/admin/coupons/index.html.erb
new file mode 100644
index 0000000..8a1201b
--- /dev/null
+++ b/app/views/admin/coupons/index.html.erb
@@ -0,0 +1,23 @@
+Coupons
+
+
+
+ Code
+ Count
+ Product
+
+
+
+<% @coupons.each do |coupon| %>
+
+ <%= coupon.attributes['code'] %>
+ <%= coupon.attributes['count'] %>
+ <%= coupon.attributes['product_code'] %>
+ <%= link_to 'Show', admin_coupon_path(coupon.attributes['code']) %>
+
+<% end %>
+
+
+
+
+<%= link_to 'New Coupon', new_admin_coupon_path %>
diff --git a/app/views/admin/coupons/new.html.erb b/app/views/admin/coupons/new.html.erb
new file mode 100644
index 0000000..7404d5c
--- /dev/null
+++ b/app/views/admin/coupons/new.html.erb
@@ -0,0 +1,5 @@
+New Coupon
+
+<%= render 'form' %>
+
+<%= link_to 'Back', admin_coupons_path %>
diff --git a/app/views/admin/coupons/show.html.erb b/app/views/admin/coupons/show.html.erb
new file mode 100644
index 0000000..30b2630
--- /dev/null
+++ b/app/views/admin/coupons/show.html.erb
@@ -0,0 +1,87 @@
+
+
+
+
+
+ Code
+ <%= @coupons.first.code %>
+
+
+ Description
+ <%= @coupons.first.description %>
+
+
+ Product Code
+ <%= @coupons.first.product_code %>
+
+
+ Discount Amount
+ <%= number_to_currency(@coupons.first.amount) %>
+
+
+
+
+
+
+
+
+ <%= link_to 'Disable All', admin_toggle_state_for_all_coupons_with_code_path(@coupons.first.code, 'disable') %>
+
+
+ <%= link_to 'Enable All', admin_toggle_state_for_all_coupons_with_code_path(@coupons.first.code, 'enable') %>
+
+
+
+
+
+
+
+
+ Coupon
+ Used Count
+ Use Limit
+ Status
+ Expiration
+
+
+
+<% @coupons.each do |coupon| %>
+
+ <%= coupon.coupon %>
+ <%= coupon.used_count %>
+ <%= coupon.use_limit %>
+ <% if ! coupon.expired? %>
+
+ <%= coupon.enabled? ? 'Enabled' : 'Disabled' %>
+
+
+ <% if coupon.expiration_date != nil %>
+ <%= distance_of_time_in_words_to_now(coupon.expiration_date) %>
+ <%= " ago" if coupon.expiration_date < Time.now %>
+ <% else %>
+ Never
+ <% end %>
+
+ <% else %>
+
+ Expired
+
+
+ <% end %>
+
+
+ <% if coupon.enabled? %>
+ <%= link_to 'Disable', admin_disable_coupon_path(coupon, 'disable') %>
+ <% else %>
+ <%= link_to 'Enable', admin_disable_coupon_path(coupon, 'enable') %>
+ <% end %>
+
+ <%= link_to 'Edit', edit_admin_coupon_path(coupon) %>
+
+<% end %>
+
+
+
+
+
+<%= link_to 'Back', admin_coupons_path %>
diff --git a/app/views/admin/generate_coupons.html.erb b/app/views/admin/generate_coupons.html.erb
new file mode 100644
index 0000000..a99d54e
--- /dev/null
+++ b/app/views/admin/generate_coupons.html.erb
@@ -0,0 +1,59 @@
+Add Coupons
+
+<%= form_tag :action => "generate_coupons" do %>
+
+
+
+<% end %>
+
+<% if @coupons %>
+Generated Coupons
+
+<% for coupon in @coupons %>
+<%= coupon.coupon %>
+<% end %>
+
+<% end %>
+
+
+
diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb
new file mode 100644
index 0000000..4e10534
--- /dev/null
+++ b/app/views/admin/index.html.erb
@@ -0,0 +1,40 @@
+
+
+
+
Summary
+
+
+
+ <%= render :partial => "revenue_summary" %>
+
+
+
+
+
History
+
+
+
+ <%= render :partial => "revenue_history" %>
+
+
diff --git a/app/views/admin/login.html.erb b/app/views/admin/login.html.erb
new file mode 100644
index 0000000..8ac47aa
--- /dev/null
+++ b/app/views/admin/login.html.erb
@@ -0,0 +1,32 @@
+
+
+Magic Potion Required
+
+<%= form_tag :action => "login" do %>
+
+
+
+ magic:
+
+
+ <%= text_field_tag "username" %>
+
+
+
+
+ potion:
+
+
+ <%= password_field_tag "password" %>
+
+
+
+
+
+ <%= submit_tag "Abracadabra" %>
+
+
+
+<% end %>
diff --git a/app/views/admin/mass_order.html.erb b/app/views/admin/mass_order.html.erb
new file mode 100644
index 0000000..2ff824a
--- /dev/null
+++ b/app/views/admin/mass_order.html.erb
@@ -0,0 +1,14 @@
+<% @qty = {} %>
+<%= form_tag :action => "mass_order" do %>
+
+<%= render :partial => "product_quantities" %>
+
+Payment Type:<%= text_field 'form', 'payment_type', :value => 'Paypal' %>
+
+Format is: "fname, lname, email"
+
+<%= text_area 'form', 'people' %>
+
+<%= submit_tag 'Process Orders' %>
+
+<% end %>
diff --git a/app/views/admin/orders/_form.html.erb b/app/views/admin/orders/_form.html.erb
new file mode 100644
index 0000000..db66309
--- /dev/null
+++ b/app/views/admin/orders/_form.html.erb
@@ -0,0 +1,66 @@
+<% if @order && !@order.new_record? %>
+<%= form.hidden_field :id %>
+<%= form.hidden_field :status %>
+<% end %>
+
diff --git a/app/views/admin/orders/_product_quantities.html.erb b/app/views/admin/orders/_product_quantities.html.erb
new file mode 100644
index 0000000..a76357d
--- /dev/null
+++ b/app/views/admin/orders/_product_quantities.html.erb
@@ -0,0 +1,19 @@
+
+ <% for product in Product.find(:all) %>
+
+ <% next if params[:action] == "new" && product.active == 0 %>
+ <% if @order && @order.has_item_with_product_id(product.id) %>
+ <% item = @order.line_item_with_product_id(product.id) %>
+ <%= item.product.name %>
+ <%= text_field "items", item.product.id, :size => 4, :value => item.quantity %>
+ $<%= text_field "item_prices", item.product.id, :size => 5, :value => item.unit_price %>
+ <%= item.license_key %>
+ <% elsif product.active == 1 %>
+ <%= product.name %>
+ <%= text_field "items", product.id, :size => 4 %>
+ $<%= text_field "item_prices", product.id, :size => 5, :value => product.price %>
+
+ <% end %>
+
+ <% end %>
+
diff --git a/app/views/admin/orders/edit.html.erb b/app/views/admin/orders/edit.html.erb
new file mode 100644
index 0000000..48f87e7
--- /dev/null
+++ b/app/views/admin/orders/edit.html.erb
@@ -0,0 +1,10 @@
+Edit Order <%= @order.id %>
+
+<%= error_messages_for :order %>
+
+<%= form_for(:order, :url => admin_order_path(@order), :html => {:method => :put}) do |f| %>
+ <%= render :partial => "form", :object => f %>
+
+ <%= submit_tag "Update" %>
+
+<% end %>
diff --git a/app/views/admin/orders/index.html.erb b/app/views/admin/orders/index.html.erb
new file mode 100644
index 0000000..b6d0a5d
--- /dev/null
+++ b/app/views/admin/orders/index.html.erb
@@ -0,0 +1,49 @@
+Orders
+
+
+
+
+
+
+ #
+ Date
+ Name
+ Email
+ Address
+ Total
+ Paytype
+ Failure
+
+<%
+ last_day = 0
+ for order in @orders %>
+
+ <%= order.status %>
+
+ <%= link_to order.id, :action => "show", :id => order %><%= "*" if !order.comment.blank? %>
+
+ <%= order.order_time.strftime("%m/%d/%y %H:%M") %>
+ <%= order.licensee_name ? order.licensee_name : order.name %>
+ <%= order.email %>
+ <% if order.cc_order? %><%= order.address %>, <%= order.city %>, <%= order.state %> <%= order.zipcode %>,<% end %> <%= order.country %>
+ <%= number_to_currency order.total %>
+ <%= order.payment_type %>
+ <% if order.failure_code %>
+ <%= order.failure_code %>: <%= order.failure_reason %>
+ <% else %>
+
+ <% end %>
+
+
+<%
+ last_day = order.order_time.yday
+ end %>
+
+
+
+
+
diff --git a/app/views/admin/orders/new.html.erb b/app/views/admin/orders/new.html.erb
new file mode 100644
index 0000000..ffeada8
--- /dev/null
+++ b/app/views/admin/orders/new.html.erb
@@ -0,0 +1,10 @@
+Add Order
+
+<%= error_messages_for :order %>
+
+<%= form_for(:order, :url => admin_orders_path) do |f| %>
+<%= render :partial => "form", :object => f %>
+
+ <%= submit_tag "Create" %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/admin/orders/show.html.erb b/app/views/admin/orders/show.html.erb
new file mode 100644
index 0000000..d74a2c9
--- /dev/null
+++ b/app/views/admin/orders/show.html.erb
@@ -0,0 +1,20 @@
+<%= render :partial => "store/order/receipt" %>
+
+
+ Status: <%= @order.status_description %>
+ Transaction ID: <%= @order.transaction_number %>
+
+
+Commands
+
+ <%= link_to "Edit Order", :action => "edit", :id => @order.id %>
+ <% if @order.complete? %>
+ <%= link_to "Cancel Order", :action => "cancel", :id => @order.id %>
+ <% elsif @order.status == "X" %>
+ <%= link_to "Refund Order", :action => "refund", :id => @order.id %>
+ <%= link_to "Uncancel Order", :action => "uncancel", :id => @order.id %>
+ <% end %>
+ <% if @order.complete? %>
+ <%= link_to "Email Purchase Receipt", :action => "send_emails", :id => @order.id %>
+ <% end %>
+
diff --git a/app/views/admin/products/_form.html.erb b/app/views/admin/products/_form.html.erb
new file mode 100644
index 0000000..dd97578
--- /dev/null
+++ b/app/views/admin/products/_form.html.erb
@@ -0,0 +1,25 @@
+<%= error_messages_for 'product' %>
+
+Code
+<%= form.text_field :code %>
+
+Name
+<%= form.text_field :name %>
+
+Price
+<%= form.text_field :price %>
+
+Image path
+<%= form.text_field :image_path , {"cols" => 40, "rows" => 3} %>
+
+Url
+<%= form.text_field :url , {"cols" => 40, "rows" => 3} %>
+
+Download url
+<%= form.text_field :download_url , {"cols" => 40, "rows" => 3} %>
+
+License url scheme
+<%= form.text_field :license_url_scheme , {"cols" => 40, "rows" => 3} %>
+
+Active
+<%= form.check_box :active , {} , "1", "0" %>
diff --git a/app/views/admin/products/edit.html.erb b/app/views/admin/products/edit.html.erb
new file mode 100644
index 0000000..ba64c85
--- /dev/null
+++ b/app/views/admin/products/edit.html.erb
@@ -0,0 +1,9 @@
+Editing product
+
+<%= form_for(:product, :url => admin_product_path(@product), :html => {:method => :put}) do |f| %>
+ <%= render :partial => 'form', :object => f %>
+ <%= submit_tag "Update" %>
+<% end %>
+
+<%= link_to 'Show', admin_product_path %> |
+<%= link_to 'Back', admin_products_path %>
diff --git a/app/views/admin/products/index.html.erb b/app/views/admin/products/index.html.erb
new file mode 100644
index 0000000..43c5d34
--- /dev/null
+++ b/app/views/admin/products/index.html.erb
@@ -0,0 +1,26 @@
+Listing products
+
+
+
+
+ Code
+ Name
+ Price
+ Action
+
+<% for product in @products %>
+
+ <%= image_tag(product.image_path) %>
+ <%=h product.code %>
+ <%=h product.name %>
+ <%=h number_to_currency(product.price) %>
+ <%= link_to 'Show', admin_product_path(product) %>
+ <%= link_to 'Edit', edit_admin_product_path(product) %>
+ <%= link_to 'Delete', admin_product_path(product), :confirm => 'Are you sure?', :method => :delete %>
+
+<% end %>
+
+
+
+
+<%= link_to 'New product', :controller => 'products', :action => 'new' %>
diff --git a/app/views/admin/products/new.html.erb b/app/views/admin/products/new.html.erb
new file mode 100644
index 0000000..cf8a9bc
--- /dev/null
+++ b/app/views/admin/products/new.html.erb
@@ -0,0 +1,10 @@
+New product
+
+<%= error_messages_for :product %>
+
+<%= form_for(:product, :url => admin_products_path) do |f| %>
+ <%= render :partial => 'form', :object => f %>
+ <%= submit_tag "Create" %>
+<% end %>
+
+<%= link_to 'Back', admin_products_path %>
diff --git a/app/views/admin/products/show.html.erb b/app/views/admin/products/show.html.erb
new file mode 100644
index 0000000..73d8331
--- /dev/null
+++ b/app/views/admin/products/show.html.erb
@@ -0,0 +1,8 @@
+<% for column in Product.content_columns %>
+
+ <%= column.human_name %>: <%=h @product.send(column.name) %>
+
+<% end %>
+
+<%= link_to 'Edit', edit_admin_product_path %> |
+<%= link_to 'Back', admin_products_path %>
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb
new file mode 100644
index 0000000..f64a8bf
--- /dev/null
+++ b/app/views/layouts/admin.html.erb
@@ -0,0 +1,32 @@
+
+
+
+
+ Admin: <%= controller.action_name %>
+ <%= stylesheet_link_tag "scaffold" %>
+ <%= stylesheet_link_tag "admin" %>
+ <%= javascript_include_tag 'application' %>
+
+
+
+ <% if session[:logged_in] %>
+
+ <% end %>
+ <%= flash[:notice] %>
+ <%= flash[:error] %>
+
+ <%= yield %>
+
+
diff --git a/app/views/layouts/error.html.erb b/app/views/layouts/error.html.erb
new file mode 100644
index 0000000..f46e8e1
--- /dev/null
+++ b/app/views/layouts/error.html.erb
@@ -0,0 +1,54 @@
+
+
+
+
+ <%=$STORE_PREFS['store_name']%>
+
+ <%= stylesheet_link_tag "store", "mytheme", :media => "all" %>
+ <%= javascript_include_tag "store" %>
+
+
+
+
+
+ <% if not $STORE_PREFS['company_logo_path'].blank? %>
+
+ <% end %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
+
+ <% if is_live?() && ! $STORE_PREFS['google_analytics_account'].blank? %>
+
+ <% end %>
+
+
diff --git a/app/views/layouts/store.html.erb b/app/views/layouts/store.html.erb
new file mode 100644
index 0000000..ad303c7
--- /dev/null
+++ b/app/views/layouts/store.html.erb
@@ -0,0 +1,53 @@
+
+
+
+
+ <%=$STORE_PREFS['store_name']%>
+
+ <%= stylesheet_link_tag "store", "mytheme", :media => "all" %>
+
+ <%= javascript_include_tag "application" %>
+
+
+
+
+ <% if not $STORE_PREFS['company_logo_path'].blank? %>
+
+
+ <% end %>
+
<%=$STORE_PREFS['store_name']%>
+
+ <%= image_tag "store/rounded_tr.png", :alt => "rounded border", :class => "tr" %>
+
+
+
+
+<% if is_live?() && ! $STORE_PREFS['google_analytics_account'].blank? %>
+
+<% end %>
+<% if is_live?() && $STORE_PREFS['include_mint'] == true %>
+
+<% end %>
+
+
diff --git a/app/views/order_mailer/lost_license_sent.html.erb b/app/views/order_mailer/lost_license_sent.html.erb
new file mode 100644
index 0000000..f5d6add
--- /dev/null
+++ b/app/views/order_mailer/lost_license_sent.html.erb
@@ -0,0 +1,29 @@
+Order Number: <%= @order.id %>
+
+Placed On: <%= @order.order_time.utc %>
+
+Registered-To: <%= @order.first_name %> <%= @order.last_name %>
+<% if not @order.company.blank? %>
+Company: <%= @order.company %>
+<% end %>
+Email: <%= @order.email %>
+
+Billing Address:
+<% if @order.payment_type == 'PayPal' %>
+N/A
+<% else %>
+<%= @order.address1 %><%= ', ' + @order.address2 if @order.address2 && ! @order.address2.blank? %>
+<%= @order.city %>, <%= @order.state %> <%= @order.zipcode %>
+<%= country_name(@order.country) %>
+<% end %>
+
+Payment:
+<%= @order.payment_type %><%= ', ' + @order.ccnum if @order.cc_order? %>
+
+Purchased Items:
+
+<% for item in @order.line_items %>
+<%= sprintf("%d %s @ %s each", item.quantity, item.product.name, number_to_currency(item.unit_price)) %>
+<% end %>
+
+Total: <%= number_to_currency(@order.total) %>
diff --git a/app/views/order_mailer/lost_license_sent.text.erb b/app/views/order_mailer/lost_license_sent.text.erb
new file mode 100644
index 0000000..f5d6add
--- /dev/null
+++ b/app/views/order_mailer/lost_license_sent.text.erb
@@ -0,0 +1,29 @@
+Order Number: <%= @order.id %>
+
+Placed On: <%= @order.order_time.utc %>
+
+Registered-To: <%= @order.first_name %> <%= @order.last_name %>
+<% if not @order.company.blank? %>
+Company: <%= @order.company %>
+<% end %>
+Email: <%= @order.email %>
+
+Billing Address:
+<% if @order.payment_type == 'PayPal' %>
+N/A
+<% else %>
+<%= @order.address1 %><%= ', ' + @order.address2 if @order.address2 && ! @order.address2.blank? %>
+<%= @order.city %>, <%= @order.state %> <%= @order.zipcode %>
+<%= country_name(@order.country) %>
+<% end %>
+
+Payment:
+<%= @order.payment_type %><%= ', ' + @order.ccnum if @order.cc_order? %>
+
+Purchased Items:
+
+<% for item in @order.line_items %>
+<%= sprintf("%d %s @ %s each", item.quantity, item.product.name, number_to_currency(item.unit_price)) %>
+<% end %>
+
+Total: <%= number_to_currency(@order.total) %>
diff --git a/app/views/order_mailer/thankyou.html.erb b/app/views/order_mailer/thankyou.html.erb
new file mode 100644
index 0000000..6deb137
--- /dev/null
+++ b/app/views/order_mailer/thankyou.html.erb
@@ -0,0 +1,78 @@
+<%
+ if @order.items_count == 1
+ application = 'application'
+ file = 'file'
+ key = 'KEY'
+ else
+ application = 'applications'
+ file = 'files'
+ key = 'KEYS'
+ end
+
+ coupons = @order.promo_coupons
+
+ if coupons.length > 0
+ vc_coupons = coupons.compact()
+ vc_coupons.reject!{|x| x.product_code != 'vc'}
+ end
+%>
+
+
+
+
+ <%=$STORE_PREFS['company_name']%> Receipt
+
+
+
+
+
+
+
Dear <%= h @order.name %>,
+
+
+ Thank you for purchasing <%=$STORE_PREFS['company_name']%> software. Your license keys are below. If you already have the <%= application %>
+ installed, you can activate by clicking the "Activate Now" link.
+ We recommend that you save this email in case you need to activate again in the future.
+
+
+
LICENSE <%= key %>
+<% for item in @order.line_items %>
+
+<%=item.product.name%>:
+<%= item.license_key %> Activate Now
+
+<% end %>
+
+
+
Please let us know if you have any questions or comments. Enjoy your purchase.
+
+
- <%=$STORE_PREFS['company_name']%>
+
+
+ <%= render :partial => '/store/order/receipt', :order => @order %>
+
+ <% if coupons && coupons.length > 0 %>
+
+
COUPONS
+ <% if vc_coupons.length != 0 %>
+
+ If you have friends who might enjoy Voice Candy, here are 3 coupon
+ codes for $3 off their purchases. They expire in 15 days.
+
+
<%= vc_coupons.collect {|x| x.coupon}.join(', ') %>
+ <% end %>
+ <% end %>
+
+
+
+
+
diff --git a/app/views/order_mailer/thankyou.text.erb b/app/views/order_mailer/thankyou.text.erb
new file mode 100644
index 0000000..81dc46e
--- /dev/null
+++ b/app/views/order_mailer/thankyou.text.erb
@@ -0,0 +1,85 @@
+<%
+ if @order.items_count == 1
+ application = 'application'
+ file = 'file'
+ else
+ application = 'applications'
+ file = 'files'
+ end
+
+ coupons = @order.promo_coupons
+%>
+Dear <%= @order.first_name %> <%= @order.last_name %>,
+
+Thank you for purchasing <%=$STORE_PREFS['company_name']%> software.
+Below are your application license keys. To activate, please copy
+and paste the code to the registration window in the application.
+We recommend that you save this email in case you need to activate
+again in the future.
+
+<% for item in @order.line_items %>
+<%=item.product.name%>:
+<%= item.license_key %>
+
+<% end %>
+
+Please let us know if you have any questions or comments.
+Enjoy your purchase.
+
+- <%=$STORE_PREFS['company_name']%>
+
+
+
+RECEIPT
+----------------------------------------------------------------------
+
+Order Number: <%= @order.id %>
+
+Placed On: <%= @order.order_time.utc %>
+
+Registered-To: <%= @order.first_name %> <%= @order.last_name %>
+<% if not @order.company.blank? %>
+Company: <%= @order.company %>
+<% end %>
+Email: <%= @order.email %>
+
+Billing Address:
+<% if @order.payment_type == 'PayPal' %>
+N/A
+<% else %>
+<%= @order.address1 %><%= ', ' + @order.address2 if not @order.address2.blank? %>
+<%= @order.city %>, <%= @order.state %> <%= @order.zipcode %>
+<%= country_name(@order.country) %>
+<% end %>
+
+Payment:
+<%= @order.payment_type %><%= ', ' + @order.ccnum if @order.cc_order? %>
+
+Purchased Items:
+
+<% for item in @order.line_items %>
+<%= sprintf("%d %s @ %s each", item.quantity, item.product.name, number_to_currency(item.unit_price)) %>
+<% end %>
+<% if @order.coupon %>
+<%= @order.coupon.description %>: -<%= number_to_currency(@order.coupon_amount) %>
+<% end %>
+
+Total: <%= number_to_currency(@order.total) %>
+
+<% if coupons && coupons.length > 0 %>
+<% vc_coupons = coupons.compact()
+ vc_coupons.reject!{|x| x.product_code != 'vc'}
+
+ if coupons.length != 0 %>
+
+
+COUPONS
+----------------------------------------------------------------------
+<% if vc_coupons.length != 0 %>
+If you have friends who might enjoy Voice Candy, here are 3 coupon
+codes for $3 off their purchases. They expire in 15 days.
+
+<%= vc_coupons.collect {|x| x.coupon}.join(', ') %>
+<% end %>
+<% end %>
+<% end %>
diff --git a/app/views/store/.DS_Store b/app/views/store/.DS_Store
new file mode 100644
index 0000000..00c3f98
Binary files /dev/null and b/app/views/store/.DS_Store differ
diff --git a/app/views/store/lost_license/index.html.erb b/app/views/store/lost_license/index.html.erb
new file mode 100644
index 0000000..89f4f57
--- /dev/null
+++ b/app/views/store/lost_license/index.html.erb
@@ -0,0 +1,30 @@
+<%= form_tag :action => "retrieve" do %>
+
+Retrieve License
+
+
+Please tell us the email address used during the purchase. Your license along with the order receipt
+will be sent by email.
+
+
+<% if flash[:license_notice] %>
+<%= h flash[:license_notice] %>
+<% end %>
+
+
+
+
+ Email address: <%= text_field_tag 'email', @email, :size => 40 %>
+ <%= submit_tag 'Submit' %>
+
+
+
+
+
+
+ If your email address has changed, let us know by email
+
+
+
+
+<% end %>
diff --git a/app/views/store/lost_license/sent.html.erb b/app/views/store/lost_license/sent.html.erb
new file mode 100644
index 0000000..62336c8
--- /dev/null
+++ b/app/views/store/lost_license/sent.html.erb
@@ -0,0 +1,9 @@
+License Sent
+
+
+Your license information has been sent. If you do not receive it within an hour, please
+check your spam folder or contact us at
+<%=$STORE_PREFS['support_email']%> .
+
+
+
diff --git a/app/views/store/order/_form_countries.html.erb b/app/views/store/order/_form_countries.html.erb
new file mode 100644
index 0000000..b214c37
--- /dev/null
+++ b/app/views/store/order/_form_countries.html.erb
@@ -0,0 +1,4 @@
+
+<%= country = @order.country.blank? ? "US" : @order.country
+ options_for_select(country_name_and_code_pairs(), country) %>
+
diff --git a/app/views/store/order/_receipt.html.erb b/app/views/store/order/_receipt.html.erb
new file mode 100644
index 0000000..cd1cdb9
--- /dev/null
+++ b/app/views/store/order/_receipt.html.erb
@@ -0,0 +1,153 @@
+<% if @print %>
+
+
+
+ <%=$STORE_PREFS['company_name']%> Receipt
+
+
+
+ <%=$STORE_PREFS['company_name']%>
+<% end %>
+
+<%
+=begin
+/* The following is the original CSS code. They have been inlined for GMail
+
+#receipt { font-family:helvetica, arial, sans-serif; font-size:12px; line-height:18px; margin-top:30px; }
+#receipt h2, #receipt h3 { margin: 0; color:#333; }
+#receipt #order_number { vertical-align:bottom; }
+#receipt table { border-collapse:collapse; }
+#receipt tr td,
+#receipt tr th { padding:5px 0; }
+#receipt td { vertical-align:top; }
+#receipt #payment { width:35%; }
+#receipt .license_key {text-align:center; }
+#receipt .unit { width:12%; text-align:center; }
+#receipt .qty { width:12%; text-align:center; }
+#receipt .price { width:11%; text-align:right; }
+#receipt th.unit { text-align:left; }
+#receipt table tr.r { border-bottom: 1px solid #ddd; }
+#receipt table tr.s td { border-bottom: 1px solid #777; padding-bottom: 12px; }
+#receipt table tr.d { border-bottom: 3px double #ddd; }
+#receipt h3 { display:inline; margin:0; padding:0;}
+
+=end %>
+
+
+
+
+
+
+ Receipt
+
+
+
+
+ Order Number: <%= @order.id %>
+
+
+
+
+
+ Time: <%= @order.order_time.utc %>
+
+
+
+
+
+ Licensed to: <%=h @order.licensee_name %>
+
+
+ <% if not @order.company.blank? %>
+ Company: <%=h @order.company %>
+ <% end %>
+
+
+
+ Email: <%=h @order.email %>
+
+
+
+
+ Billing Address:
+ <%=h @order.first_name %> <%=h @order.last_name %>
+ <% if @order.address1.blank? %>
+ N/A
+ <% else %>
+ <%=h @order.address1
+ %><%=h ', ' + @order.address2 if @order.address2 && ! @order.address2.blank? %>
+ <%=h @order.city %>, <%=h @order.state %> <%=h @order.zipcode %>
+ <%=h country_name(@order.country) %>
+ <% end %>
+
+
+ Payment:
+ <%=h @order.payment_type %>
+ <% if @order.cc_order? %>
+ <%=h @order.ccnum %>
+ <% end %>
+
+
+
+
+
+ Item
+ License Key
+ Unit Price
+ Qty
+ Price
+
+ <% for item in @order.line_items %>
+
+
+ <%=h item.product.name %>
+
+
+ <%=h item.license_key %>
+
+
+ <%= number_to_currency item.unit_price %>
+
+
+ <%= item.quantity %>
+
+
+ <%= number_to_currency item.unit_price * item.quantity %>
+
+
+ <% end %>
+ <% if @order.coupon %>
+
+ <%= @order.coupon.description %>
+ -<%= number_to_currency(@order.coupon_amount) %>
+
+ <% end %>
+
+
+
+ Total:
+
+
+ <%= number_to_currency @order.total %>
+
+
+ <% if @order.comment %>
+
+
+ Comment: <%=h @order.comment %>
+
+
+ <% end %>
+
+
+<% if @print %>
+
+
+<% end %>
diff --git a/app/views/store/order/confirm_paypal.html.erb b/app/views/store/order/confirm_paypal.html.erb
new file mode 100644
index 0000000..6172ac2
--- /dev/null
+++ b/app/views/store/order/confirm_paypal.html.erb
@@ -0,0 +1,106 @@
+
+
+Confirm
+
+<% unless @order.errors.empty? %>
+
+
Problems
+
+ <% @order.errors.each_full do |message| %>
+ <%= message %>
+ <% end %>
+
+
+<% end %>
+
+
+
+
+ <% if @order.items_count == 1 %>
+
Your Item
+ <% else %>
+
Your Items
+ <% end %>
+
+
+ <% for item in @order.line_items %>
+ <% if item.quantity > 0 %>
+
+ <%= item.quantity %> @ <%= number_to_currency item.unit_price %> each
+ <%= item.product.name %>
+
+ <% end %>
+ <% end %>
+ <% if @order.coupon %>
+
+ -<%= number_to_currency(@order.coupon_amount) %>
+ <%= @order.coupon.description %>
+
+ <% end %>
+
+
+ Total: <%= number_to_currency @order.total %>
+
+
+
+
+<%= form_tag({:action => "purchase_paypal"}, {:id => "purchase_paypal"}) do %>
+
+
+
+
+
Your Information
+
Name on License: <%= text_field "order", "licensee_name", :class => "required" %>
+
+
Email: <%= text_field "order", "email", :class => "required" %>
+
+
+ Company: <%= text_field "order", "company" %>
+
+
+
+ Comment: <%= text_field "order", "comment" %>
+
+
+
+ Keep me updated with <%=$STORE_PREFS['company_name']%> news.
+
+
+
+
+<% end %>
diff --git a/app/views/store/order/failed.html.erb b/app/views/store/order/failed.html.erb
new file mode 100644
index 0000000..870ac11
--- /dev/null
+++ b/app/views/store/order/failed.html.erb
@@ -0,0 +1,30 @@
+Order Failed
+
+
+
+
+ <% if @order.status == 'C' %>
+ You placed this order already.
+ <% elsif @order.status == 'X' %>
+ This order has been cancelled.
+ <% elsif @order.failure_reason == nil %>
+ This order failed for an unknown reason
+ <% else %>
+ <%= @order.failure_reason.gsub("\n", " ").html_safe %>
+ <% end %>
+
+
+
+<% if @order.country == "US" && @order.status != "C" %>
+
+ Non-USA Customers: Please make sure to select your country
+
+<% end %>
+
+
+ <% if @order.status == 'C' %>
+ An order receipt with the license key was sent to <%= @order.email %>
+ <% else %>
+ Go back to try again
+ <% end %>
+
diff --git a/app/views/store/order/new.html.erb b/app/views/store/order/new.html.erb
new file mode 100644
index 0000000..83cfd0a
--- /dev/null
+++ b/app/views/store/order/new.html.erb
@@ -0,0 +1,79 @@
+Purchase Software
+
+
+
+<%= form_tag :action => "payment" do %>
+
+
+ If you need a site license or have special pricing needs, please
contact us .
+
+
+
+<% for product in @products %>
+
+
+
+ <% if not product.image_path.blank? %>
+
+ <% end %>
+ <%= product.name %>
+
+
+
+ <%= text_field "items", product.id, :size => "3", :value => @qty[product.code], :autocomplete => "off", :class => "qty" %>
+ @ <%= number_to_currency product.price %> each
+
+
+<% end %>
+
+
+ Coupon Code:
+ <%= text_field_tag 'coupon', session[:coupon_text], :autocomplete => 'off' %>
+
+
+
+ Payment Method:
+
+
+ <%= radio_button_tag 'payment_type', 'cc', !(['paypal', 'gcheckout'].member? @payment_type), :id => 'creditcard' %>
+ <%= image_tag "store/visa.gif", :alt => "Visa" %>
+ <%= image_tag "store/mc.gif", :alt => "MasterCard" %>
+ <%= image_tag "store/amex.gif", :alt => "Amex" %>
+ <%= image_tag "store/discover.gif", :alt => "Discover" %>
+
+
+ <%= radio_button_tag 'payment_type', 'paypal', @payment_type == 'paypal', :id => 'paypal' %>
+ <%= image_tag "store/paypal.gif", :alt => "Paypal" %>
+ Shop without sharing your financial information
+
+ <% if $STORE_PREFS['allow_google_checkout'] then %>
+
+ <%= radio_button_tag 'payment_type', 'gcheckout', @payment_type == 'gcheckout', :id => 'gcheckout' %>
+ <%= image_tag "store/gcheckout.gif", :alt => "Google Checkout" %>
+
+ <% end %>
+
+
+
+
+
+
+ <%= flash[:notice] %>
+
+
+
+
+<% end %>
diff --git a/app/views/store/order/no_order.html.erb b/app/views/store/order/no_order.html.erb
new file mode 100644
index 0000000..1112972
--- /dev/null
+++ b/app/views/store/order/no_order.html.erb
@@ -0,0 +1,6 @@
+Order Failed
+
+We could not find your incomplete order in our database.
+This can happen if you have cookies disabled or if your session has expired.
+
+<%= link_to 'Go Back to the Store', :action => 'index' %>
diff --git a/app/views/store/order/payment_cc.html.erb b/app/views/store/order/payment_cc.html.erb
new file mode 100644
index 0000000..be70af1
--- /dev/null
+++ b/app/views/store/order/payment_cc.html.erb
@@ -0,0 +1,213 @@
+
+
+Checkout
+
+<% unless @order.errors.empty? %>
+
+
Problems
+
+ <% @order.errors.each_full do |message| %>
+ <%= message %>
+ <% end %>
+
+
+<% end %>
+
+
+
+
+ <% if @order.items_count == 1 %>
+
Your Item
+ <% else %>
+ Your Items
+ <% end %>
+
+
+
+ <% for item in @order.line_items %>
+ <% if item.quantity > 0 %>
+
+ <%= item.quantity %> @ <%= number_to_currency item.unit_price %> each
+ <%= item.product.name %>
+
+ <% end %>
+ <% end %>
+ <% if @order.coupon %>
+
+ -<%= number_to_currency(@order.coupon_amount) %>
+ <%= @order.coupon.description %>
+
+ <% end %>
+
+
+ Total: <%= number_to_currency @order.total %>
+
+
+
+<%= form_tag({:action => "purchase"}, {:id => "purchase"}) do %>
+
+<% for item in @order.line_items %>
+<%= hidden_field "items", String(item.product_id), :value => item.quantity %>
+<% end %>
+<%= hidden_field "order", "uuid" %>
+<%= hidden_field_tag("coupon", @order.coupon.coupon) if @order.coupon %>
+
+
+
+
Billing Information
+
First Name <%= text_field "order", "first_name", :size=>15, :class => "required" %>
+
Last Name <%= text_field "order", "last_name", :size=>15, :class => "required" %>
+
+
+ Company: <%= text_field "order", "company" %>
+
+
+
Address Line 1:
+
Address Line 2:
+
+
+ City: <%= text_field "order", "city", :size=>15 %>
+ State:<%= text_field "order", "state", :size=>4 %>
+
+
+
+ Postal/Zip Code: <%= text_field "order", "zipcode", :size=>10 %>
+
+
+ Country: <%= render :partial => "form_countries" %>
+
+
+ Email: <%= text_field "order", "email", :class => "required email" %>
+
+
+
+
+
+
+
Credit Card
+
Card Type:
+
+ <%= image_tag "store/visa.gif", :alt => 'Visa', :width => 37, :height => 23 %>
+
+
+ <%= image_tag "store/mc.gif", :alt => 'MasterCard', :width => 37, :height => 23 %>
+
+
+ <%= image_tag "store/amex.gif", :alt => 'Amex', :width => 37, :height => 23 %>
+
+
+ <%= image_tag"store/discover.gif", :alt => 'Discover', :width => 37, :height => 23 %>
+
+
+
Credit Card Number: <%= text_field "order", "cc_number", :size=>24, :class => "required creditcard" %>
+
+
+ Expiration Date:
+ <%= text_field "order", "cc_month", :maxlength => 2, :class => "required" %> /<%= text_field "order", "cc_year", :maxlength=>2, :class => "required" %>
+ CSC: <%= text_field "order", "cc_code", :size =>4 %>
+ <%= image_tag "store/cvv.png", :alt => "Security Code", :width => 37, :height => 21 %>
+
+
+
+
+
+
+
Almost There
+
Name on License: <%= text_field "order", "licensee_name", :class => "required" %>
+
Comment: <%= text_field "order", "comment" %>
+
+ Keep me updated with <%=$STORE_PREFS['company_name']%> news.
+
+
+
+
+<% end %>
diff --git a/app/views/store/order/payment_gcheckout.html.erb b/app/views/store/order/payment_gcheckout.html.erb
new file mode 100644
index 0000000..65caabf
--- /dev/null
+++ b/app/views/store/order/payment_gcheckout.html.erb
@@ -0,0 +1,114 @@
+
+
+Confirm
+
+<% unless @order.errors.empty? %>
+
+
Problems
+
+ <% @order.errors.each_full do |message| %>
+ <%= message %>
+ <% end %>
+
+
+<% end %>
+
+Your license key, along with your purchase receipt, will be emailed to your Gmail address.
+
+
+
+
+ <% if @order.items_count == 1 %>
+
Your Item
+ <% else %>
+
Your Items
+ <% end %>
+
+
+ <% for item in @order.line_items %>
+ <% if item.quantity > 0 %>
+
+ <%= item.quantity %> @ <%= number_to_currency item.unit_price %> each
+ <%= item.product.name %>
+
+ <% end %>
+ <% end %>
+ <% if @order.coupon %>
+
+ -<%= number_to_currency(@order.coupon_amount) %>
+ <%= @order.coupon.description %>
+
+ <% end %>
+
+
+ Total: <%= number_to_currency @order.total %>
+
+
+
+
+
+<% if not $STORE_PREFS['google_analytics_account'].blank? %>
+
+<% end %>
+
+<%= form_tag({:action => "purchase"}, {:id => "purchase"}) do %>
+<% for item in @order.line_items %>
+<%= hidden_field 'items', String(item.product_id), :value=> item.quantity %>
+<% end %>
+<%= hidden_field_tag("coupon", @order.coupon_text) if @order.coupon %>
+<%= hidden_field("order", "payment_type", :value => "Google Checkout") %>
+<% if not $STORE_PREFS['google_analytics_account'].blank? %>
+
+<% end %>
+
+
+
+
+
Your Information
+
+ Name on License: <%= text_field "order", "licensee_name", :class => "required" %>
+
+
+ Comment: <%= text_field "order", "comment" %>
+
+
+
+
+
+
+
+<% end %>
+
+
diff --git a/app/views/store/order/thankyou.html.erb b/app/views/store/order/thankyou.html.erb
new file mode 100644
index 0000000..c513057
--- /dev/null
+++ b/app/views/store/order/thankyou.html.erb
@@ -0,0 +1,95 @@
+<%
+ if @order.items_count == 1
+ application = 'application'
+ icon = 'icon'
+ file = 'file'
+ it = 'it'
+ else
+ application = 'applications'
+ icon = 'icons'
+ file = 'files'
+ it = 'them'
+ end
+
+ coupons = @order.promo_coupons
+ vc_coupons = coupons.compact()
+ vc_coupons.reject!{|x| x.product_code != 'vc'}
+
+%>
+Thank You
+
+
+ Thank you for purchasing <%=$STORE_PREFS['company_name']%> software. If you have not
+ downloaded the software already, please do so now:
+
+
+
+
+HOW TO ACTIVATE
+
+
+ If you have the <%= application %> installed, you can activate by clicking on the "Activate Now" button.
+ Otherwise, you can copy and paste the license key into the application's registration window.
+
+
+
+ <% for item in @order.line_items %>
+
+ <%=item.product.name%>:
+ <%= item.license_key %>
+ ACTIVATE NOW
+
+<% end %>
+
+
+
+
+
+<%= render :partial => 'receipt' %>
+
+
+ <%= link_to image_tag('store/printer.png')+' Printer Friendly Receipt', { :action => 'receipt' }, :popup => ['new_window', 'height=500,width=600'] %>
+
+
+<% if coupons.length != 0 %>
+DISCOUNT COUPONS
+<% end
+ if vc_coupons.length != 0 %>
+
+ If you have friends who might enjoy Voice Candy, here are 3 coupon
+ codes for $3 off their purchases. They expire in 15 days.
+
+
+<%= vc_coupons.collect {|x| x.coupon}.join(', ') %>
+<% end %>
+
+
+ The contents of this page are being sent to you by email.
+
+
+<% if is_live?() && ! $STORE_PREFS['google_analytics_account'].blank? %>
+
+
+
+<% end %>
diff --git a/app/views/support_mailer/crash_report.html.erb b/app/views/support_mailer/crash_report.html.erb
new file mode 100644
index 0000000..10126a7
--- /dev/null
+++ b/app/views/support_mailer/crash_report.html.erb
@@ -0,0 +1 @@
+<%= @report %>
diff --git a/app/views/support_mailer/support_request.html.erb b/app/views/support_mailer/support_request.html.erb
new file mode 100644
index 0000000..17a3d7c
--- /dev/null
+++ b/app/views/support_mailer/support_request.html.erb
@@ -0,0 +1 @@
+<%= @message %>
\ No newline at end of file
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..1c1b0b4
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Potionstore::Application
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..7c660c6
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,64 @@
+require File.expand_path('../boot', __FILE__)
+
+require 'rails/all'
+
+if defined?(Bundler)
+ # If you precompile assets before deploying to production, use this line
+ Bundler.require *Rails.groups(:assets => %w(development test))
+ # If you want your assets lazily compiled in production, use this line
+ # Bundler.require(:default, :assets, Rails.env)
+end
+
+module Potionstore
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence those specified here
+
+ # Skip frameworks you're not going to use
+ # config.frameworks -= [ :action_web_service, :action_mailer ]
+ #config.frameworks -= [ :action_web_service ]
+
+ # Add additional load paths for your own custom dirs
+ # config.autoload_paths += %W( #{Rails.root}/extras )
+
+ # Force all environments to use the same logger level
+ # (by default production uses :info, the others :debug)
+ # config.log_level = :debug
+
+ # Use the database for sessions instead of the file system
+ # (create the session table with 'rake db:sessions:create')
+ # config.action_controller.session_store = :active_record_store
+
+ # If wanting Rails 2 with cookie store:
+ #config.action_controller.session = { :key => "_potion_store_session", :secret => "sdaf0022s94hfdbz32sdjfhf4j-=123sdh" }
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Activate observers that should always be running
+ # config.active_record.observers = :cacher, :garbage_collector
+
+ # Make Active Record use UTC-base instead of local time
+ # config.active_record.default_timezone = :utc
+
+ # See Rails::Configuration for more options
+
+ # Enable the asset pipeline
+ config.assets.enabled = true
+
+ # Version of your assets, change this if you want to expire all your assets
+ config.assets.version = '1.0'
+
+ config.assets.precompile += ['*.css', '*.js']
+
+ # Change the path that assets are served from
+ # config.assets.prefix = "/assets"
+
+ # Configure the default encoding used in templates for Ruby 1.9.
+ config.encoding = "utf-8"
+
+ # Configure sensitive parameters which will be filtered from the log file.
+ config.filter_parameters += [:password, :cc_number, :cc_code, :cc_month, :cc_year]
+ end
+end
\ No newline at end of file
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..ab6cb37
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,13 @@
+require 'rubygems'
+
+# Set up gems listed in the Gemfile.
+gemfile = File.expand_path('../../Gemfile', __FILE__)
+begin
+ ENV['BUNDLE_GEMFILE'] = gemfile
+ require 'bundler'
+ Bundler.setup
+rescue Bundler::GemNotFound => e
+ STDERR.puts e.message
+ STDERR.puts "Try running `bundle install`."
+ exit!
+end if File.exist?(gemfile)
diff --git a/config/certs/api_cert_chain.crt b/config/certs/api_cert_chain.crt
new file mode 100644
index 0000000..347f0eb
--- /dev/null
+++ b/config/certs/api_cert_chain.crt
@@ -0,0 +1,54 @@
+-----BEGIN CERTIFICATE-----
+MIIDgzCCAuygAwIBAgIQJUuKhThCzONY+MXdriJupDANBgkqhkiG9w0BAQUFADBf
+MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xNzA1BgNVBAsT
+LkNsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
+HhcNOTcwNDE3MDAwMDAwWhcNMTExMDI0MjM1OTU5WjCBujEfMB0GA1UEChMWVmVy
+aVNpZ24gVHJ1c3QgTmV0d29yazEXMBUGA1UECxMOVmVyaVNpZ24sIEluYy4xMzAx
+BgNVBAsTKlZlcmlTaWduIEludGVybmF0aW9uYWwgU2VydmVyIENBIC0gQ2xhc3Mg
+MzFJMEcGA1UECxNAd3d3LnZlcmlzaWduLmNvbS9DUFMgSW5jb3JwLmJ5IFJlZi4g
+TElBQklMSVRZIExURC4oYyk5NyBWZXJpU2lnbjCBnzANBgkqhkiG9w0BAQEFAAOB
+jQAwgYkCgYEA2IKA6NYZAn0fhRg5JaJlK+G/1AXTvOY2O6rwTGxbtueqPHNFVbLx
+veqXQu2aNAoV1Klc9UAl3dkHwTKydWzEyruj/lYncUOqY/UwPpMo5frxCTvzt01O
+OfdcSVq4wR3Tsor+cDCVQsv+K1GLWjw6+SJPkLICp1OcTzTnqwSye28CAwEAAaOB
+4zCB4DAPBgNVHRMECDAGAQH/AgEAMEQGA1UdIAQ9MDswOQYLYIZIAYb4RQEHAQEw
+KjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL0NQUzA0BgNV
+HSUELTArBggrBgEFBQcDAQYIKwYBBQUHAwIGCWCGSAGG+EIEAQYKYIZIAYb4RQEI
+ATALBgNVHQ8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgEGMDEGA1UdHwQqMCgwJqAk
+oCKGIGh0dHA6Ly9jcmwudmVyaXNpZ24uY29tL3BjYTMuY3JsMA0GCSqGSIb3DQEB
+BQUAA4GBAAgB7ORolANC8XPxI6I63unx2sZUxCM+hurPajozq+qcBBQHNgYL+Yhv
+1RPuKSvD5HKNRO3RrCAJLeH24RkFOLA9D59/+J4C3IYChmFOJl9en5IeDCSk9dBw
+E88mw0M9SR2egi5SX7w+xmYpAY5Okiy8RnUDgqxz6dl+C2fvVFIa
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkG
+A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
+cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
+MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
+BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
+YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
+BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
+I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
+CSqGSIb3DQEBAgUAA4GBALtMEivPLCYATxQT3ab7/AoRhIzzKBxnki98tsX63/Do
+lbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59AhWM1pF+NEHJwZRDmJXNyc
+AA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2OmufTqj/ZA1k
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
+BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
+c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
+MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
+emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
+DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
+FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg
+UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
+YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
+MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4
+pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0
+13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID
+AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk
+U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i
+F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY
+oJ2daZH9
+-----END CERTIFICATE-----
diff --git a/config/certs/live_api.crt b/config/certs/live_api.crt
new file mode 100644
index 0000000..e69de29
diff --git a/config/certs/live_api.key b/config/certs/live_api.key
new file mode 100644
index 0000000..e69de29
diff --git a/config/certs/sandbox_api.crt b/config/certs/sandbox_api.crt
new file mode 100644
index 0000000..e69de29
diff --git a/config/certs/sandbox_api.key b/config/certs/sandbox_api.key
new file mode 100644
index 0000000..e69de29
diff --git a/config/countries.yml b/config/countries.yml
new file mode 100644
index 0000000..957f7f4
--- /dev/null
+++ b/config/countries.yml
@@ -0,0 +1,191 @@
+# List of countries supported by PayPal Website Payments Pro
+US: "United States"
+AL: "Albania"
+DZ: "Algeria"
+AD: "Andorra"
+AO: "Angola"
+AI: "Anguilla"
+AG: "Antigua and Barbuda"
+AR: "Argentina"
+AM: "Armenia"
+AW: "Aruba"
+AU: "Australia"
+AT: "Austria"
+AZ: "Azerbaijan Republic"
+BS: "Bahamas"
+BH: "Bahrain"
+BB: "Barbados"
+BE: "Belgium"
+BZ: "Belize"
+BJ: "Benin"
+BM: "Bermuda"
+BT: "Bhutan"
+BO: "Bolivia"
+BA: "Bosnia and Herzegovina"
+BW: "Botswana"
+BR: "Brazil"
+VG: "British Virgin Islands"
+BN: "Brunei"
+BG: "Bulgaria"
+BF: "Burkina Faso"
+BI: "Burundi"
+KH: "Cambodia"
+CA: "Canada"
+CV: "Cape Verde"
+KY: "Cayman Islands"
+TD: "Chad"
+CL: "Chile"
+CN: "China"
+CO: "Colombia"
+KM: "Comoros"
+CK: "Cook Islands"
+CR: "Costa Rica"
+HR: "Croatia"
+CY: "Cyprus"
+CZ: "Czech Republic"
+CD: "Democratic Republic of the Congo"
+DK: "Denmark"
+DJ: "Djibouti"
+DM: "Dominica"
+DO: "Dominican Republic"
+EC: "Ecuador"
+SV: "El Salvador"
+ER: "Eritrea"
+EE: "Estonia"
+ET: "Ethiopia"
+FK: "Falkland Islands"
+FO: "Faroe Islands"
+FM: "Federated States of Micronesia"
+FJ: "Fiji"
+FI: "Finland"
+FR: "France"
+GF: "French Guiana"
+PF: "French Polynesia"
+GA: "Gabon Republic"
+GM: "Gambia"
+DE: "Germany"
+GI: "Gibraltar"
+GR: "Greece"
+GL: "Greenland"
+GD: "Grenada"
+GP: "Guadeloupe"
+GT: "Guatemala"
+GN: "Guinea"
+GW: "Guinea Bissau"
+GY: "Guyana"
+HN: "Honduras"
+HK: "Hong Kong"
+HU: "Hungary"
+IS: "Iceland"
+IN: "India"
+ID: "Indonesia"
+IE: "Ireland"
+IL: "Israel"
+IT: "Italy"
+JM: "Jamaica"
+JP: "Japan"
+JO: "Jordan"
+KZ: "Kazakhstan"
+KE: "Kenya"
+KI: "Kiribati"
+KW: "Kuwait"
+KG: "Kyrgyzstan"
+LA: "Laos"
+LV: "Latvia"
+LS: "Lesotho"
+LI: "Liechtenstein"
+LT: "Lithuania"
+LU: "Luxembourg"
+MG: "Madagascar"
+MW: "Malawi"
+MY: "Malaysia"
+MV: "Maldives"
+ML: "Mali"
+MT: "Malta"
+MH: "Marshall Islands"
+MQ: "Martinique"
+MR: "Mauritania"
+MU: "Mauritius"
+YT: "Mayotte"
+MX: "Mexico"
+MN: "Mongolia"
+MS: "Montserrat"
+MA: "Morocco"
+MZ: "Mozambique"
+NA: "Namibia"
+NR: "Nauru"
+NP: "Nepal"
+NL: "Netherlands"
+AN: "Netherlands Antilles"
+NC: "New Caledonia"
+NZ: "New Zealand"
+NI: "Nicaragua"
+NE: "Niger"
+NU: "Niue"
+NF: "Norfolk Island"
+"NO": "Norway"
+OM: "Oman"
+PW: "Palau"
+PA: "Panama"
+PG: "Papua New Guinea"
+PE: "Peru"
+PH: "Philippines"
+PN: "Pitcairn Islands"
+PL: "Poland"
+PT: "Portugal"
+QA: "Qatar"
+CG: "Republic of the Congo"
+RE: "Reunion"
+RO: "Romania"
+RU: "Russia"
+RW: "Rwanda"
+VC: "Saint Vincent and the Grenadines"
+WS: "Samoa"
+SM: "San Marino"
+ST: "São Tomé and Príncipe"
+SA: "Saudi Arabia"
+SN: "Senegal"
+SC: "Seychelles"
+SL: "Sierra Leone"
+SG: "Singapore"
+SK: "Slovakia"
+SI: "Slovenia"
+SB: "Solomon Islands"
+SO: "Somalia"
+ZA: "South Africa"
+KR: "South Korea"
+ES: "Spain"
+LK: "Sri Lanka"
+SH: "St. Helena"
+KN: "St. Kitts and Nevis"
+LC: "St. Lucia"
+PM: "St. Pierre and Miquelon"
+SR: "Suriname"
+SJ: "Svalbard and Jan Mayen Islands"
+SZ: "Swaziland"
+SE: "Sweden"
+CH: "Switzerland"
+TW: "Taiwan"
+TJ: "Tajikistan"
+TZ: "Tanzania"
+TH: "Thailand"
+TG: "Togo"
+TO: "Tonga"
+TT: "Trinidad and Tobago"
+TN: "Tunisia"
+TR: "Turkey"
+TM: "Turkmenistan"
+TC: "Turks and Caicos Islands"
+TV: "Tuvalu"
+UG: "Uganda"
+UA: "Ukraine"
+AE: "United Arab Emirates"
+GB: "United Kingdom"
+UY: "Uruguay"
+VU: "Vanuatu"
+VA: "Vatican City State"
+VE: "Venezuela"
+VN: "Vietnam"
+WF: "Wallis and Futuna Islands"
+YE: "Yemen"
+ZM: "Zambia"
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..b604aeb
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,23 @@
+production:
+ adapter: mysql2
+ encoding: utf8
+ database: store
+ username: root
+ password:
+ timeout: 5000
+
+development:
+ adapter: mysql2
+ encoding: utf8
+ database: store
+ username: root
+ password:
+ timeout: 5000
+
+test:
+ adapter: mysql2
+ encoding: utf8
+ database: store-test
+ username: root
+ password:
+ timeout: 5000
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..fe2f584
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,7 @@
+# Load the rails application
+require File.expand_path('../application', __FILE__)
+
+require 'digest/md5'
+
+# Initialize the rails application
+Potionstore::Application.initialize!
\ No newline at end of file
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..4c54a7d
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,31 @@
+Potionstore::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the webserver when you make code changes.
+ config.cache_classes = false
+
+ # Log error messages when you accidentally call methods on nil.
+ config.whiny_nils = true
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send
+ config.action_mailer.raise_delivery_errors = false
+
+ # Print deprecation notices to the Rails logger
+ config.active_support.deprecation = :log
+
+ # Only use best-standards-support built into browsers
+ config.action_dispatch.best_standards_support = :builtin
+
+ # Do not compress assets
+ config.assets.compress = false
+
+ # Expands the lines which load the assets
+ config.assets.debug = true
+end
+
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..e1d9f84
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,69 @@
+Potionstore::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # The production environment is meant for finished, "live" apps.
+ # Code is not reloaded between requests
+ config.cache_classes = true
+
+ # Full error reports are disabled and caching is turned on
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Specifies the header that your server uses for sending files
+ #config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+ # For nginx:
+ config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
+ #config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+ # If you have no front-end server that supports something like X-Sendfile,
+ # just comment this out and Rails will serve the files
+
+ # See everything in the log (default is :info)
+ # config.log_level = :debug
+
+ # Use a different logger for distributed setups
+ # config.logger = SyslogLogger.new
+
+ # Use a different cache store in production
+ # config.cache_store = :mem_cache_store
+
+ # Disable Rails's static asset server
+ # In production, Apache or nginx will already do this
+ config.serve_static_assets = false
+
+ # Enable serving of images, stylesheets, and javascripts from an asset server
+ # config.action_controller.asset_host = "http://assets.example.com"
+
+ # Disable delivery errors, bad email addresses will be ignored
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable threaded mode
+ # config.threadsafe!
+
+ # Compress JavaScripts and CSS
+ config.assets.compress = true
+
+ # Choose the compressors to use
+ # config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :yui
+
+ # Don't fallback to assets pipeline if a precompiled asset is missed
+ config.assets.compile = false
+
+ # Generate digests for assets URLs.
+ config.assets.digest = true
+
+ # Defaults to Rails.root.join("public/assets")
+ # config.assets.manifest = YOUR_PATH
+
+ # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
+ # config.assets.precompile += %w( search.js )
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation can not be found)
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners
+ config.active_support.deprecation = :notify
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..60ffae4
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,35 @@
+Potionstore::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Log error messages when you accidentally call methods on nil.
+ config.whiny_nils = true
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Print deprecation notices to the stderr
+ config.active_support.deprecation = :stderr
+end
diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb
new file mode 100644
index 0000000..d1dd565
--- /dev/null
+++ b/config/initializers/exception_notification.rb
@@ -0,0 +1,5 @@
+Rails.application.config.middleware.use ExceptionNotifier,
+ :email_prefix => "[STORE]",
+ :sender_address => %{"Potion Store" },
+ :exception_recipients => %{storecrash@domain.com}
+
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
new file mode 100644
index 0000000..35685de
--- /dev/null
+++ b/config/initializers/secret_token.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+Potionstore::Application.config.secret_token = '2b60145fae93221e0b69e7a0b29ba099cfe651ca35cb1625abcbcaf7e2cf8344be19e5e64da51138cb9dadb80c5b65a9ccf7301b86b4c6cff86e795ca598f2bb'
diff --git a/config/preinitializer.rb b/config/preinitializer.rb
new file mode 100644
index 0000000..85399f7
--- /dev/null
+++ b/config/preinitializer.rb
@@ -0,0 +1,20 @@
+begin
+ require "rubygems"
+ require "bundler"
+rescue LoadError
+ raise "Could not load the bundler gem. Install it with `gem install bundler`."
+end
+
+if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24")
+ raise RuntimeError, "Your bundler version is too old for Rails 2.3." +
+ "Run `gem install bundler` to upgrade."
+end
+
+begin
+ # Set up load paths for all bundled gems
+ ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
+ Bundler.setup
+rescue Bundler::GemNotFound
+ raise RuntimeError, "Bundler couldn't find some gems." +
+ "Did you run `bundle install`?"
+end
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..0146b8c
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,42 @@
+Potionstore::Application.routes.draw do
+ match 'store' => 'store/order#new'
+ match '' => 'store/order#index'
+
+ scope "store" do
+ match "order/payment" => "store/order#payment"
+ match "order/purchase" => "store/order#purchase"
+ match "order/thankyou" => "store/order#thankyou"
+ match "order/receipt" => "store/order#receipt"
+ match "order/purchase_paypal" => "store/order#purchase_paypal"
+ match "order/confirm_paypal" => "store/order#confirm_paypal"
+ resources :order, :singular => true, :module => "store"
+
+ # lost license routes
+ match 'lost_license' => 'store/lost_license#index'
+ match 'lost_license/retrieve' => 'store/lost_license#retrieve'
+ match 'lost_license/sent' => 'store/lost_license#sent'
+
+ # google checkout
+ match 'notification/gcheckout' => 'store/notification#gcheckout'
+ end
+
+ namespace :admin do
+ resources :products
+ resources :coupons
+ match 'coupons/:id/:operation' => 'coupons#toggle_state', :constraints => { :operation => /disable|enable/ }, :as => 'disable_coupon'
+ match 'coupons/:id/toggle_state_for_all_coupons_with_code/:operation' => 'coupons#toggle_state_for_all_coupons_with_code', :constraints => { :operation => /disable|enable/ }, :as => 'toggle_state_for_all_coupons_with_code'
+ #match 'coupons/:id/delete_all' => 'coupons#delete_all_coupons_with_code', :as => 'delete_all_coupons_with_code'
+ resources :orders do
+ member do
+ get :cancel
+ get :uncancel
+ get :refund
+ get :send_emails
+ end
+ end
+ end
+
+ match 'admin/charts/:action' => 'admin/charts#index'
+ match 'bugreport/crash' => 'email#crash_report'
+ match '/:controller(/:action(/:id))'
+end
\ No newline at end of file
diff --git a/config/store.yml b/config/store.yml
new file mode 100644
index 0000000..17a8cf7
--- /dev/null
+++ b/config/store.yml
@@ -0,0 +1,40 @@
+# Admin site login
+admin_username: "admin"
+admin_password: "password"
+
+# Presentation stuf
+company_name: "My Company"
+company_url: "http://www.mycompany.com/"
+company_logo_path: ""
+store_name: "My Company Store"
+copyright_html: "Copyright © 2011 My Company. All rights reserved."
+google_analytics_account: ""
+include_mint: false
+paypal_express_checkout_header_image: "https://www.mycompany.com/images/mylogo.png"
+
+# Google checkout?
+allow_google_checkout: true
+
+# Email addresses
+support_email: "support@mycompany.com"
+sales_email: "sales@mycompany.com"
+
+purchase_receipt_sender_email: "My Company "
+purchase_receipt_bcc_email: "orders@mycompany.com"
+
+send_lost_license_sent_notification_email: false
+lost_license_sent_notification_sender_email: "My Company "
+lost_license_sent_notification_recipient_email: "info@mycompany.com"
+
+crash_report_sender_email: "bugs@mycompany.com"
+crash_report_recipient_email: "bugs@mycompany.com"
+
+# Using SSL?
+#
+# Turning this on redirects most requests that come in through http to https
+# In addition, if you're using Apache, you might need to add the following
+# to its SSL configuration:
+#
+# RequestHeader set X_FORWARDED_PROTO 'https'
+#
+redirect_to_ssl: true
diff --git a/db/migrate/001_create_tables.rb b/db/migrate/001_create_tables.rb
new file mode 100644
index 0000000..bb56a25
--- /dev/null
+++ b/db/migrate/001_create_tables.rb
@@ -0,0 +1,107 @@
+class CreateTables < ActiveRecord::Migration
+ def self.up
+
+ create_table :products do |t|
+ t.column "code", :string, :limit => 16, :default => "", :null => false
+ t.column "name", :string, :limit => 64, :default => "", :null => false
+ t.column "price", :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false
+ t.column "image_path", :text
+ t.column "url", :text
+ t.column "download_url", :text
+ t.column "license_url_scheme",:text
+ t.column "active", :integer, :default => 1, :null => false
+ end
+
+ create_table :coupons do |t|
+ t.column "code", :string, :limit => 16, :default => "", :null => false
+ t.column "description", :string, :limit => 64, :default => "", :null => false
+ t.column "coupon", :string, :limit => 64, :default => "", :null => false
+ t.column "product_code", :string, :limit => 16, :default => "", :null => false
+ t.column "amount", :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false
+ t.column "percentage", :integer
+ t.column "used_count", :integer
+ t.column "use_limit", :integer, :default => 1, :null => false
+ t.column "creation_time", :timestamp
+ t.column "numdays", :integer, :default => 0, :null => false
+ end
+
+ add_index "coupons", ["coupon"], :name => "coupon"
+
+ create_table :orders do |t|
+ t.column "coupon_id", :integer
+ t.column "status", :string, :limit => 1, :default => "P", :null => false
+ t.column "email", :string, :limit => 128, :default => "", :null => false
+ t.column "order_time", :datetime
+ t.column "first_name", :string, :limit => 64, :default => ""
+ t.column "licensee_name", :string, :limit => 128
+ t.column "last_name", :string, :limit => 64, :default => ""
+ t.column "company", :string, :limit => 64
+ t.column "address1", :string, :limit => 64, :default => ""
+ t.column "address2", :string, :limit => 64
+ t.column "city", :string, :limit => 64, :default => ""
+ t.column "state", :string, :limit => 64, :default => ""
+ t.column "zipcode", :string, :limit => 64, :default => ""
+ t.column "country", :string, :limit => 2, :default => "", :null => false
+ t.column "payment_type", :string, :limit => 16
+ t.column "ccnum", :string, :limit => 32
+ t.column "comment", :text
+ t.column "failure_code", :integer
+ t.column "failure_reason", :string
+ t.column "transaction_number", :string, :limit => 64
+ end
+
+ add_index "orders", ["coupon_id"], :name => "coupon_id"
+ add_index "orders", ["email"], :name => "email"
+
+ create_table :line_items do |t|
+ t.column "order_id", :integer, :default => 0, :null => false
+ t.column "product_id", :integer, :default => 0, :null => false
+ t.column "quantity", :integer, :default => 0, :null => false
+ t.column "unit_price", :decimal, :precision => 10, :scale => 2, :default => 0.0, :null => false
+ t.column "license_key", :string, :limit => 64
+ end
+
+ add_index "line_items", ["order_id"], :name => "order_id"
+ add_index "line_items", ["product_id"], :name => "product_id"
+
+ create_table :list_subscribers do |t|
+ t.column "email", :text, :default => "", :null => false
+ end
+
+ add_foreign_key :line_items, :orders, :dependent => :delete
+ add_foreign_key :line_items, :products
+ add_foreign_key :orders, :coupons
+
+ p = Product.new
+ p.code = "foo"
+ p.name = "Footion v1"
+ p.price = 12.95
+ p.image_path = "/images/store/application_icon.png"
+ p.url = "http://www.mycompany.com/foo/"
+ p.download_url = "http://www.mycompany.com/foo/download/"
+ p.license_url_scheme = "x-com-mycompany-license-footion"
+ p.save()
+
+ p = Product.new
+ p.code = "bar"
+ p.name = "Barsoap v1"
+ p.price = 24.95
+ p.url = "http://www.mycompany.com/bar/"
+ p.image_path = "/images/store/application_icon.png"
+ p.download_url = "http://www.mycompany.com/bar/download/"
+ p.license_url_scheme = "x-com-mycompany-license-barsoap"
+ p.save()
+ end
+
+ def self.down
+ remove_foreign_key :line_items, :orders, :dependent => :delete
+ remove_foreign_key :line_items, :products
+ remove_foreign_key :orders, :coupons
+
+ drop_table :list_subscribers
+ drop_table :line_items
+ drop_table :orders
+ drop_table :coupons
+ drop_table :products
+ end
+end
diff --git a/db/migrate/20110109204308_add_uid_to_orders.rb b/db/migrate/20110109204308_add_uid_to_orders.rb
new file mode 100644
index 0000000..fa5e206
--- /dev/null
+++ b/db/migrate/20110109204308_add_uid_to_orders.rb
@@ -0,0 +1,21 @@
+# Add a UID to all orders
+require 'uid'
+
+class AddUidToOrders < ActiveRecord::Migration
+ def self.up
+ add_column :orders, :unique_id, :string, :limit => 16
+ Order.reset_column_information
+
+ Order.find(:all).each do |o|
+ o.uuid = uid()
+ o.skip_cc_validation = true
+ o.save!
+ end
+
+ change_column :orders, :unique_id, :string, :limit => 16, :null => false
+ end
+
+ def self.down
+ remove_column :orders, :uuid
+ end
+end
diff --git a/db/migrate/20110112001916_add_total_to_orders.rb b/db/migrate/20110112001916_add_total_to_orders.rb
new file mode 100644
index 0000000..792fbaa
--- /dev/null
+++ b/db/migrate/20110112001916_add_total_to_orders.rb
@@ -0,0 +1,23 @@
+def round_money(amount)
+ return ("%01.2f" % amount).to_f()
+end
+
+class AddTotalToOrders < ActiveRecord::Migration
+ def self.up
+ add_column :orders, :total, :decimal, :precision => 10, :scale => 2
+
+ Order.reset_column_information
+
+ Order.find(:all).each do |o|
+ o.total = o.calculated_total
+ o.skip_cc_validation = true
+ o.save!
+ end
+
+ change_column :orders, :total, :decimal, :precision => 10, :scale => 2, :null => false
+ end
+
+ def self.down
+ remove_column :orders, :total
+ end
+end
diff --git a/db/migrate/20120306035217_add_columns_to_coupons.rb b/db/migrate/20120306035217_add_columns_to_coupons.rb
new file mode 100644
index 0000000..89ebab8
--- /dev/null
+++ b/db/migrate/20120306035217_add_columns_to_coupons.rb
@@ -0,0 +1,11 @@
+class AddColumnsToCoupons < ActiveRecord::Migration
+ def change
+ add_column :coupons, :enabled, :boolean, :default => true
+ add_column :coupons, :expiration_date, :timestamp
+ end
+
+ def self.down
+ remove_column :coupons, :enabled, :boolean
+ remove_column :coupons, :expiration_date, :timestamp
+ end
+end
diff --git a/db/migrate/20120327023036_rename_order_unique_id.rb b/db/migrate/20120327023036_rename_order_unique_id.rb
new file mode 100644
index 0000000..1443f0a
--- /dev/null
+++ b/db/migrate/20120327023036_rename_order_unique_id.rb
@@ -0,0 +1,9 @@
+class RenameOrderUniqueId < ActiveRecord::Migration
+ def up
+ rename_column :orders, :unique_id, :uuid
+ end
+
+ def down
+ rename_column :orders, :uuid, :unique_id
+ end
+end
diff --git a/db/migrate/20120327023053_increase_order_uuid_column_size.rb b/db/migrate/20120327023053_increase_order_uuid_column_size.rb
new file mode 100644
index 0000000..685a766
--- /dev/null
+++ b/db/migrate/20120327023053_increase_order_uuid_column_size.rb
@@ -0,0 +1,9 @@
+class IncreaseOrderUuidColumnSize < ActiveRecord::Migration
+ def up
+ change_column :orders, :uuid, :string, :limit => 36
+ end
+
+ def down
+ change_column :orders, :uuid, :string, :limit => 16
+ end
+end
diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP
new file mode 100644
index 0000000..ac6c149
--- /dev/null
+++ b/doc/README_FOR_APP
@@ -0,0 +1,2 @@
+Use this README file to introduce your application and point to useful places in the API for learning more.
+Run "rake appdoc" to generate API documentation for your models and controllers.
\ No newline at end of file
diff --git a/lib/licensekey.rb b/lib/licensekey.rb
new file mode 100644
index 0000000..d5a549e
--- /dev/null
+++ b/lib/licensekey.rb
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+#
+# Modify this file and put in your own license key generator
+#
+
+def random_chars_of_length(len)
+ chars = ("A".."Z").to_a + ("0".."9").to_a
+ result = ""
+ 1.upto(len) { |i| result << chars[rand(chars.size-1)] }
+ return result
+end
+
+def make_license(product_code, name, copies)
+ license = product_code.upcase() + '-' + random_chars_of_length(16)
+ return license
+end
+
+# Simple command line test
+if __FILE__ == $0
+ puts make_license('tgr', 'Andy Kim', 1)
+end
diff --git a/lib/ruby-paypal.rb b/lib/ruby-paypal.rb
new file mode 100644
index 0000000..83a2bcc
--- /dev/null
+++ b/lib/ruby-paypal.rb
@@ -0,0 +1,6 @@
+# == About ruby-paypal.rb
+#
+# Ruby-PayPal is a lightweight wrapper around the PayPal NVP APIs
+
+require 'ruby-paypal/paypal'
+require 'ruby-paypal/credit_card_checks'
\ No newline at end of file
diff --git a/lib/ruby-paypal/credit_card_checks.rb b/lib/ruby-paypal/credit_card_checks.rb
new file mode 100644
index 0000000..ef1cdeb
--- /dev/null
+++ b/lib/ruby-paypal/credit_card_checks.rb
@@ -0,0 +1,58 @@
+#
+# Performs a series of credit card number checks.
+#
+module CreditCardChecks
+ # Perform Luhn check on a credit card number. Refer to http://en.wikipedia.org/wiki/Luhn_algorithm
+ #
+ def luhn_check(number)
+ num = 0
+ number.length.times { |p|
+ position = number.length - p - 1
+ if position % 2 == 0 then
+ doubled = number[position,1].to_i * 2
+ if doubled > 9 then
+ num = num + doubled.to_s[0,1].to_i + doubled.to_s[1,1].to_i
+ else
+ num = num + doubled.to_i
+ end
+ else
+ num = num + number[position,1].to_i
+ end
+ }
+ if num.to_i % 10 == 0 then
+ return true
+ else
+ return false
+ end
+ end
+
+ # Perform checks on credit card numbers with reference to the length of the credit card number
+ # and the type of credit card. Refer to http://en.wikipedia.org/wiki/Credit_card_number
+ #
+ def card_type_check(type, number)
+ validity = false
+ case type.upcase
+ when "VISA"
+ validity = true if (number.length == 16 or number.length = 13) and number[0,1] == "4"
+ when "MASTERCARD"
+ validity = true if (number.length == 16) and (52..55).include?(number[0,2].to_i)
+ when "AMEX"
+ validity = true if (number.length == 16) and ['34', '37'].include?(number[0,2])
+ when "DISCOVER"
+ validity = true if (number.length == 16) and (number[0,2] == "65" or number[0,4] == "6011")
+ when "SWITCH"
+ # TODO checks for Switch cards
+ validity = true
+ when "SOLO"
+ # TODO checks for Solo cards
+ validity = true
+ else
+ raise "Invalid card type entered"
+ end
+
+ return validity
+ end
+
+
+
+end
\ No newline at end of file
diff --git a/lib/ruby-paypal/paypal.rb b/lib/ruby-paypal/paypal.rb
new file mode 100644
index 0000000..4b50f0f
--- /dev/null
+++ b/lib/ruby-paypal/paypal.rb
@@ -0,0 +1,667 @@
+require 'pp'
+require 'net/http'
+require 'net/https'
+require 'uri'
+require 'ruby-paypal/credit_card_checks'
+
+SANDBOX_SERVER = 'https://api-3t.sandbox.paypal.com/nvp'
+PRODUCTION_SERVER = 'https://api-3t.paypal.com/nvp'
+API_VERSION = '63.0'
+
+module Net
+ #
+ # A convenience class to enable this library to call PayPal's HTTPS NVP APIs
+ #
+ class HTTPS < HTTP
+ def initialize(address, port = nil, verify = :no_verify)
+ super(address, port)
+ self.use_ssl = true
+ self.verify_mode = OpenSSL::SSL::VERIFY_NONE if verify == :no_verify
+ end
+ end
+end
+
+#
+# A container for the response from PayPal. Each call to PayPal returns a generic set
+# of information as well as a specific set for the call. For more information please refer
+# to PayPal NVP API Developer Guide and Reference.
+#
+# To use retrieve information in the response, call the corresponding name of the object. For
+# example, all responses from PayPal includes the field ACK . To get the data for this
+# field:
+#
+#
+# if response.ack == 'Success' then
+# # do your stuff
+# end
+#
+# This is because this class uses a meta-programming trick with method_missing to redirect all
+# known method calls to its internal hash data structure.
+#
+class PayPalResponse < Hash
+ def method_missing(m,*a)
+ if m.to_s.upcase =~ /=$/
+ self[$`] = a[0]
+ elsif a.empty?
+ self[m.to_s.upcase]
+ else
+ raise NoMethodError, "#{m}"
+ end
+ end
+end
+
+
+# Container used for mass payment
+#
+class PayPalPayment
+ attr_accessor :email, :receiver_id, :uuid, :note, :amount
+end
+
+
+=begin rdoc
+Author:: Chang Sau Sheong (mailto:sausheong.chang@gmail.com)
+Author:: Philippe F. Monnet (mailto:pfmonnet@gmail.com)
+Copyright:: Copyright (c) 2007-2009 Chang Sau Sheong & Philippe F. Monnet
+License:: Distributes under the same terms as Ruby
+Version:: 0.0.5
+
+:main: Paypal
+
+=Installing Ruby-PayPal
+A lightweight ruby wrapper for PayPal NVP (Name-Value Pair) APIs. To install type the following
+at the command line:
+
+ $ gem install ruby-paypal
+
+
+=Using Ruby-PayPal
+It's critical that you understand how PayPal works and how the PayPal NVP API
+works. You should be relatively well-versed in the NVP API Developer Guide and
+Reference - see:
+- https://www.paypal.com/en_US/ebook/PP_NVPAPI_DeveloperGuide/index.html
+- https://cms.paypal.com/us/cgi-bin/?&cmd=_render-content&content_ID=developer/e_howto_api_soap_NVPAPIOverview
+
+You should also visit and register yourself with the PayPal Developer Network
+and get a Sandbox account with in the PayPal Development Central
+(https://developer.paypal.com/).
+
+Note that this library only supports the API signature method of securing the API credentials.
+
+By setting Paypal.debug=true, the API will "pretty-print" the PayPal parameters to the console.
+
+==Direct Payment
+To use credit card payment through PayPal, you need to use the DoDirectPayment APIs:
+
+ username =
+ password =
+ signature =
+
+ ipaddress = '192.168.1.1' # can be any IP address
+ amount = '100.00' # amount paid
+ card_type = 'VISA' # can be Visa, Mastercard, Amex etc
+ card_no = '4512345678901234' # credit card number
+ exp_date = '022010' # expiry date of the credit card
+ first_name = 'Sau Sheong'
+ last_name = 'Chang'
+
+ paypal = Paypal.new(username, password, signature) # uses the PayPal sandbox
+ response = paypal.do_direct_payment_sale(ipaddress, amount, card_type,
+ card_no, exp_date, first_name, last_name)
+ if response.ack == 'Success' then
+ # do your thing
+ end
+
+The above code is for a final sale only.
+
+Note that the credit card number is checked against a modulo-10 algorithm (Luhn check) as well as a simple credit card
+type check. For more information please refer to http://en.wikipedia.org/wiki/Luhn_algorithm and
+http://en.wikipedia.org/wiki/Credit_card_number
+
+==Express Checkout
+To use the customer's PayPal account for payment, you will need to use the ExpressCheckout APIs:
+
+
+
+==PayPal Subscriptions
+
+PayPal Subscriptions is a service offering allowing you to sell subscriptions, consisting of an initial payment followed by
+several recurring payments. For a good technical overview, review the following guide:
+- https://www.paypal.com/en_US/ebook/PP_ExpressCheckout_IntegrationGuide/RecurringPayments.html
+
+Using the subscriptions service involve understanding the series of exchanges to perform using the NVP API.
+There are 3 key phases of a subscription:
+1. Creating a subscription request (and button for the customer)
+2. Customer review and confirmation on the PayPal site
+3. Processing of a subscription agreement
+Each phase involves specific APIs.
+
+===Phase 1 - Subscription Request
+In this phase, the do_set_express_checkout method will be called. PayPal will return a token we can use in subsequent API calls.
+
+Let's create a subcription request with the details of our subscription:
+
+ subscription_request = create_monthly_subscription_request(
+ name='_Why's Ruby Camping Adventures',
+ id='MNWRCA',
+ description='_Why's Ruby Camping Adventures - Monthly Tips And Tricks For Camping Development',
+ invoice_number='INV20091122',
+ amount='5.00')
+
+Let's call do_set_express_checkout to get a token back:
+
+ response = paypal.do_set_express_checkout(
+ return_url='http://www.yoursite.com/subscription-confirmed',
+ cancel_url='http://www.yoursite.com/subscription-aborted',
+ amount='5.00',
+ other_params=subscription_request)
+
+ token = (response.ack == 'Success') ? response['TOKEN'] : ''
+
+Let's use the token to create a PayPal button to request payment via the sandbox:
+
+ form( { :method => 'post' ,
+ :action => 'https://www.sandbox.paypal.com/cgi-bin/webscr' #sandbox
+ } ) do
+
+ input :id => 'cmd', :name => 'cmd', :type => 'hidden',
+ :value => "_express-checkout";
+
+ input :id => 'token', :name => 'token', :type => 'hidden',
+ :value => "#{token}";
+
+ input :id => 'submit_subscription_request', :name => 'submit', :type => 'submit',
+ :value => 'Subscribe Via PayPal'
+ end #form
+
+===Phase 2 - Customer Review and Confirmation
+
+The customer will see the details of the subscription agreement we created previously.
+Upon confirmation, PayPal will redirect the customer to the return_url we specified passing
+the token back as well as the payerid.
+
+===Phase 3 = Subscription Processing
+
+First we will retrieve the details of the check-out:
+
+ response = paypal.do_get_express_checkout_details(token)
+
+Then we will execute the actual payment:
+
+ response = paypal.do_express_checkout_payment(token=token,
+ payment_action='Sale',
+ payer_id=payerid,
+ amount='5.00')
+
+ transaction_id = response['TRANSACTIONID']
+
+Now we can create the actual PayPal subscription
+
+ response = @paypal.do_create_recurring_payments_profile(token,
+ start_date='2009-11-22 14:30:10',
+ profile_reference='INV20091122',
+ description='_Why's Ruby Camping Adventures - Monthly Tips And Tricks For Camping Development',
+ billing_period='Month',
+ billing_frequency=1,
+ total_billing_cycles=11,
+ amount='5.00',
+ currency='USD')
+
+ profile_id = @response['PROFILEID']
+
+The profile_id can then be used in the future to access the details of the subscription,
+suspend it, reactivate it or cancel it using the following methods:
+- do_get_recurring_payments_profile_details
+
+ response = paypal.do_get_recurring_payments_profile_details(profile_id)
+
+- do_manage_recurring_payments_profile_status
+
+ # Suspend
+ response = paypal.do_manage_recurring_payments_profile_status(profile_id,
+ action='Suspend',
+ note='The subscription is being suspended due to payment cancellation by the customer')
+
+ # Re-Activate
+ response = paypal.do_manage_recurring_payments_profile_status(profile_id,
+ action='Reactivate',
+ note='The subscription is being reactivated due to new payment by the customer')
+
+ # Cancel
+ response = paypal.do_manage_recurring_payments_profile_status(profile_id,
+ action='Cancel',
+ note='The subscription is being cancelled due to cancellation of the account by the customer')
+
+The customer information associated with the subscription can be retrieved using:
+- do_get_billing_agreement_customer_details
+
+ response = paypal.do_get_billing_agreement_customer_details(token)
+
+Note: all subscriptions methods also accept an optional other_params hash for any other NVP you need to pass.
+
+
+=More information
+Check for updates in our blogs:
+- http://blog.saush.com
+- http://blog.monnet-usa.com
+=end
+
+class PayPal
+ include CreditCardChecks
+
+ @@debug = false
+
+ def self.debug
+ @@debug
+ end
+
+ # Controls whether or not PP debug statements will be produced and sent to the console
+ #
+ def self.debug=(val) #:doc:
+ @@debug = val
+ end
+
+ def self.express_checkout_redirect_url(token, useraction = nil)
+ live = Rails.env == 'production'
+ if live
+ url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{token}"
+ else
+ url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=#{token}"
+ end
+ url << "&useraction=#{useraction}" if useraction
+ return url
+ end
+
+ def self.make_nvp_call(params)
+ self.new.make_nvp_call(params)
+ end
+
+ # Create a new object with the given user name, password and signature. To enable production
+ # access to PayPal change the url to the live PayPal server. Set url to :production to change
+ # access to PayPal production servers.
+ #
+ def initialize()
+ environment = Rails.env
+ app_root = File.dirname(__FILE__) + '/../..'
+ config_dir = app_root + '/config'
+
+ prefs = File.expand_path(config_dir + '/paypal.yml')
+ if File.exists?(prefs)
+ y = YAML.load(File.open(prefs))
+ y.each {|pref, value| eval("@#{pref} ='#{value}'")}
+ y[environment].each {|pref, value| eval("@#{pref} =\"#{value}\"")}
+ end
+
+ @api_parameters = {
+ 'USER' => @api_username,
+ 'PWD' => @api_password,
+ 'VERSION' => API_VERSION,
+ 'SIGNATURE' => @api_signature
+ }
+
+ if environment == 'production'
+ @paypal_url = PRODUCTION_SERVER
+ else
+ @paypal_url = SANDBOX_SERVER
+ end
+ end
+
+ # Performs credit card payment with PayPal, but only requesting for authorization. You need
+ # to capture the funds later. Calls do_direct_payment.
+ #
+ # Equivalent of DoDirectPayment with the PAYMENTACTION of 'authorization'
+ #
+ def do_direct_payment_authorization(ipaddress, amount, credit_card_type, credit_card_no, expiry_date,
+ first_name, last_name, cvv2=nil, other_params={})
+ do_direct_payment('Authorization', ipaddress, amount, credit_card_type, credit_card_no,
+ expiry_date, first_name, last_name, cvv2, other_params)
+ end
+
+ # Performs credit card payment with PayPal, finalizing the sale. Funds are captured immediately.
+ # Calls do_direct_payment.
+ #
+ # Equivalent of DoDirectPayment with the PAYMENTACTION of 'sale'
+ #
+ def do_direct_payment_sale(ipaddress, amount, credit_card_type, credit_card_no, expiry_date,
+ first_name, last_name, cvv2=nil, other_params={})
+ do_direct_payment('Sale', ipaddress, amount, credit_card_type, credit_card_no,
+ expiry_date, first_name, last_name, cvv2, other_params)
+ end
+
+ # Performs credit card payment with PayPal.
+ #
+ # Equivalent of DoDirectPayment.
+ #
+ # Performs Luhn check and a simple credit card type check based on the card number.
+ #
+ def do_direct_payment(payment_action, ipaddress, amount, credit_card_type,
+ credit_card_no, expiry_date, first_name, last_name, cvv2=nil, other_params={})
+ params = {
+ 'METHOD' => 'DoDirectPayment',
+ 'PAYMENTACTION' => payment_action,
+ 'AMT' => amount.to_s,
+ 'CREDITCARDTYPE' => credit_card_type,
+ 'ACCT' => credit_card_no,
+ 'EXPDATE' => expiry_date,
+ 'FIRSTNAME' => first_name,
+ 'LASTNAME' => last_name,
+ 'IPADDRESS' => ipaddress }
+ params['CVV2'] = cvv2 unless cvv2.nil?
+ params.merge! other_params
+
+ raise 'Invalid credit card number' if not luhn_check(params['ACCT'])
+ raise 'Invalid credit card type' if not card_type_check(params['CREDITCARDTYPE'], params['ACCT'])
+
+ make_nvp_call(params)
+ end
+
+ # Performs payment through PayPal.
+ #
+ # Equivalent of SetExpressCheckout.
+ #
+ def do_set_express_checkout(return_url, cancel_url, amount, other_params={})
+ return set_express_checkout(return_url, cancel_url, amount, other_params)
+ end
+
+ def set_express_checkout(return_url, cancel_url, amount, other_params={})
+ params = {
+ 'METHOD' => 'SetExpressCheckout',
+ 'RETURNURL' => return_url,
+ 'CANCELURL' => cancel_url,
+# 'AMT' => amount.to_s # AMT is deprecated in version 63. AK 1/8/2011
+ 'PAYMENTREQUEST_0_AMT' => amount.to_s
+ }
+ params.merge! other_params
+ make_nvp_call(params)
+ end
+
+ # Gets the details of the request started through set_express_checkout.
+ #
+ # Equivalent of GetExpressCheckoutDetails.
+ #
+ def get_express_checkout_details(token)
+ params = {
+ 'METHOD' => 'GetExpressCheckoutDetails',
+ 'TOKEN' => token
+ }
+ make_nvp_call(params)
+ end
+
+ #
+ # Gets payment through PayPal for Express Checkout.
+ #
+ # Equivalent of DoExpressCheckoutPayment
+ #
+ def do_express_checkout_payment(token, payment_action, payer_id, amount, other_params={})
+ params = {
+ 'METHOD' => 'DoExpressCheckoutPayment',
+ 'TOKEN' => token,
+ 'PAYMENTACTION' => payment_action,
+ 'PAYERID' => payer_id,
+ 'AMT' => amount
+ }
+
+ params.merge! other_params
+ make_nvp_call(params)
+ end
+
+ #
+ # Does authorization of a request.
+ #
+ # Equivalent of DoAuthorization.
+ #
+ def do_authorization(transaction_id, amount, currency_code = 'USD')
+ params = {
+ 'METHOD' => 'DoAuthorization',
+ 'TRANSACTIONID' => transaction_id,
+ 'AMT' => amount.to_s,
+ 'TRANSACTIONENTITY' => 'Order',
+ 'CURRENCYCODE' => currency_code
+ }
+ make_nvp_call(params)
+ end
+
+ #
+ # Captures payment for a transaction.
+ #
+ # Equivalent of DoCapture.
+ #
+ def do_capture(authorization_id, amount, complete=true, currency_code='USD', invoice_no=nil, note=nil, soft_descriptor=nil)
+ params = {
+ 'METHOD' => 'DoCapture',
+ 'AUTHORIZATIONID' => authorization_id,
+ 'AMT' => amount.to_s,
+ 'CURRENCYCODE' => currency_code
+ }
+ if complete then
+ params['COMPLETETYPE'] = 'Complete'
+ else
+ params['COMPLETETYPE'] = 'NotComplete'
+ end
+ params['INVNUM'] = invoice_no unless invoice_no.nil?
+ params['NOTE'] = note unless note.nil?
+ params['SOFTDESCRIPTOR'] = soft_descriptor unless soft_descriptor.nil?
+ make_nvp_call(params)
+ end
+
+ #
+ # Re-authorizes an authorized transaction.
+ #
+ # Equivalent of DoReauthorization.
+ #
+ def do_reauthorization(authorization_id, amount, currency_code = 'USD')
+ params = {
+ 'METHOD' => 'DoReauthorization',
+ 'AUTHORIZATIONID' => authorization_id,
+ 'AMT' => amount.to_s,
+ 'CURRENCYCODE' => currency_code
+ }
+ make_nvp_call(params)
+ end
+
+ #
+ # Makes the call to the PayPal NVP API. This is the workhorse method for the other method calls.
+ #
+ def make_nvp_call(params)
+ pp params if @@debug
+
+ @api_parameters.merge! params
+
+ response = Net::HTTPS.post_form(URI.parse(@paypal_url), @api_parameters)
+ response.error! unless response.kind_of? Net::HTTPSuccess
+ PayPalResponse.new.merge get_hash(response.body)
+ end
+
+ #
+ # Checks and returns information on the card based on the given BIN (Bank Identification Number).
+ # Currently inactive since bindatabase.com is down.
+ #
+ def bin_check(bin)
+ # stub for check to bindatabase.com, currently down
+ end
+
+
+ # Perform mass payment to a group of recipients
+ #
+ # Equivalent to MassPay
+ #
+ def do_mass_payment(payments, email_subject, receiver_type='EmailAddress', currency_code='USD')
+ if receiver_type != 'EmailAddress' then
+ receiver_type = 'UserID'
+ end
+
+ params = {
+ 'METHOD' => 'MassPay',
+ 'RECEIVERTYPE' => receiver_type,
+ 'CURRENCYCODE' => currency_code,
+ 'EMAILSUBJECT' => email_subject
+ }
+
+ payments.each_index { |num|
+ if receiver_type == 'EmailAddress' then
+ params["L_EMAIL#{num}"] = payments[num].email
+ else
+ params["L_RECEIVERID#{num}"] = payments[num].receiver_id
+ end
+ params["L_UNIQUEID#{num}"] = payments[num].uuid
+ params["L_NOTE#{num}"] = payments[num].note
+ params["L_AMT#{num}"] = payments[num].amount
+ }
+
+ make_nvp_call(params)
+ end
+
+ # techarch> Subscription APIs
+
+ # Creates a payment subscription based on a start date, billing period, frequency, number of periods and amount
+ #
+ # Equivalent to CreateRecurringPaymentsProfile
+ #
+ def do_create_recurring_payments_profile(token, start_date, profile_reference, description, billing_period, billing_frequency, total_billing_cycles, amount, currency, other_params={})
+ params = {
+ 'METHOD' => 'CreateRecurringPaymentsProfile',
+ 'TOKEN' => token,
+ 'PROFILESTARTDATE' => start_date,
+ 'PROFILEREFERENCE' => profile_reference,
+ 'DESC' => description,
+ 'BILLINGPERIOD' => billing_period,
+ 'BILLINGFREQUENCY' => billing_frequency,
+ 'TOTALBILLINGCYCLES' => total_billing_cycles,
+ 'AMT' => amount,
+ 'CURRENCYCODE' => currency
+ }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Retrieves the details of a payment subscription for a given profile id
+ # Will return for e.g. the start date, billing period, frequency, number of periods and amount
+ #
+ # Equivalent to GetRecurringPaymentsProfileDetails
+ #
+ def do_get_recurring_payments_profile_details (profile_id, other_params={})
+ params = {
+ 'METHOD' => 'GetRecurringPaymentsProfileDetails',
+ 'PROFILEID' => profile_id }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Manages a recurring subscription profile in terms of status:
+ # - Cancel
+ # - Suspend
+ # - Reactivate
+ # Equivalent to ManageRecurringPaymentsProfileStatus
+ #
+ def do_manage_recurring_payments_profile_status(profile_id, action, note='', other_params={})
+ params = {
+ 'METHOD' => 'ManageRecurringPaymentsProfileStatus',
+ 'PROFILEID' => profile_id,
+ 'ACTION' => action,
+ 'NOTE' => note
+ }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Retrieves the customer details for the billing agreement associated with the current token
+ # Equivalent to GetBillingAgreementCustomerDetails
+ #
+ def do_get_billing_agreement_customer_details(token, other_params={})
+ params = {
+ 'METHOD' => 'GetBillingAgreementCustomerDetails',
+ 'TOKEN' => token
+ }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Initiates the creation of a billing agreement
+ # Equivalent to SetCustomerBillingAgreement
+ #
+ def do_set_billing_agreement_customer_details(return_url, cancel_url, billing_desc, billing_type='RecurringPayments', payment_type='', custom='', other_params={})
+ params = {
+ 'METHOD' => 'SetCustomerBillingAgreement',
+ 'RETURNURL' => return_url,
+ 'CANCELURL' => cancel_url,
+ 'L_BILLINGAGREEMENTDESCRIPTION0' => billing_desc,
+ 'L_BILLINGTYPE0' => billing_type,
+ 'L_PAYMENTTYPE0' => payment_type,
+ 'L_BILLINGAGREEMENTCUSTOM0' => custom
+ }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Retrieves the details of a transaction for a given transaction id
+ #
+ # Equivalent to GetTransactionDetails
+ #
+ def do_get_transaction_details (transaction_id, other_params={})
+ params = {
+ 'METHOD' => 'GetTransactionDetails',
+ 'TRANSACTIONID' => transaction_id }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Retrieves the details of a express checkout for a given token
+ #
+ # Equivalent to GetExpressCheckoutDetails
+ #
+ def do_get_express_checkout_details (token, other_params={})
+ params = {
+ 'METHOD' => 'GetExpressCheckoutDetails',
+ 'TOKEN' => token }
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # Search transactions between payee and payer
+ # Equivalent to TransactionSearch
+ #
+ def do_transaction_search(start_date,payee_email, payer_email='', payer_first='', payer_middle='', payer_last='',
+ transaction_class='Subscription', other_params={})
+ params = {
+ 'METHOD' => 'TransactionSearch',
+ 'STARTDATE' => start_date,
+ 'RECEIVER' => payee_email,
+ 'TRANSACTIONCLASS' => transaction_class
+ }
+
+ if !payer_email.nil? && !payer_email.empty?
+ params['EMAIL'] = payer_email
+ else
+ params['FIRSTNAME'] = payer_first !payer_first.nil? && !payer_first.empty?
+ params['MIDDLENAME'] = payer_middle !payer_middle.nil? && !payer_middle.empty?
+ params['LASTNAME'] = payer_last !payer_last.nil? && !payer_last.empty?
+ end
+
+ params.merge! other_params
+
+ make_nvp_call(params)
+ end
+
+ # --------------------------------------------------------------------------------------------------------------------------------------
+
+ private
+
+ #
+ # Gets a hash from a string, with a set of name value pairs joined by '='
+ # and concatenated with '&'
+ #
+ def get_hash(string)
+ hash = {}
+ string.split('&').collect { |pair| pair.split('=') }.each { |a|
+ hash[a[0]] = URI.unescape(a[1])
+ }
+ return hash
+ end
+
+ end
diff --git a/lib/tasks/capistrano.rake b/lib/tasks/capistrano.rake
new file mode 100644
index 0000000..d83042f
--- /dev/null
+++ b/lib/tasks/capistrano.rake
@@ -0,0 +1,102 @@
+# =============================================================================
+# A set of rake tasks for invoking the Capistrano automation utility.
+# =============================================================================
+
+# Invoke the given actions via Capistrano
+def cap(*parameters)
+ begin
+ require 'rubygems'
+ rescue LoadError
+ # no rubygems to load, so we fail silently
+ end
+
+ require 'capistrano/cli'
+
+ STDERR.puts "Capistrano/Rake integration is deprecated."
+ STDERR.puts "Please invoke the 'cap' command directly: `cap #{parameters.join(" ")}'"
+
+ Capistrano::CLI.new(parameters.map { |param| param.to_s }).execute!
+end
+
+namespace :remote do
+ desc "Removes unused releases from the releases directory."
+ task(:cleanup) { cap :cleanup }
+
+ desc "Used only for deploying when the spinner isn't running."
+ task(:cold_deploy) { cap :cold_deploy }
+
+ desc "A macro-task that updates the code, fixes the symlink, and restarts the application servers."
+ task(:deploy) { cap :deploy }
+
+ desc "Similar to deploy, but it runs the migrate task on the new release before updating the symlink."
+ task(:deploy_with_migrations) { cap :deploy_with_migrations }
+
+ desc "Displays the diff between HEAD and what was last deployed."
+ task(:diff_from_last_deploy) { cap :diff_from_last_deploy }
+
+ desc "Disable the web server by writing a \"maintenance.html\" file to the web servers."
+ task(:disable_web) { cap :disable_web }
+
+ desc "Re-enable the web server by deleting any \"maintenance.html\" file."
+ task(:enable_web) { cap :enable_web }
+
+ desc "A simple task for performing one-off commands that may not require a full task to be written for them."
+ task(:invoke) { cap :invoke }
+
+ desc "Run the migrate rake task."
+ task(:migrate) { cap :migrate }
+
+ desc "Restart the FCGI processes on the app server."
+ task(:restart) { cap :restart }
+
+ desc "A macro-task that rolls back the code and restarts the application servers."
+ task(:rollback) { cap :rollback }
+
+ desc "Rollback the latest checked-out version to the previous one by fixing the symlinks and deleting the current release from all servers."
+ task(:rollback_code) { cap :rollback_code }
+
+ desc "Sets group permissions on checkout."
+ task(:set_permissions) { cap :set_permissions }
+
+ desc "Set up the expected application directory structure on all boxes"
+ task(:setup) { cap :setup }
+
+ desc "Begin an interactive Capistrano session."
+ task(:shell) { cap :shell }
+
+ desc "Enumerate and describe every available task."
+ task(:show_tasks) { cap :show_tasks, '-q' }
+
+ desc "Start the spinner daemon for the application (requires script/spin)."
+ task(:spinner) { cap :spinner }
+
+ desc "Update the 'current' symlink to point to the latest version of the application's code."
+ task(:symlink) { cap :symlink }
+
+ desc "Updates the code and fixes the symlink under a transaction"
+ task(:update) { cap :update }
+
+ desc "Update all servers with the latest release of the source code."
+ task(:update_code) { cap :update_code }
+
+ desc "Update the currently released version of the software directly via an SCM update operation"
+ task(:update_current) { cap :update_current }
+
+ desc "Execute a specific action using capistrano"
+ task :exec do
+ unless ENV['ACTION']
+ raise "Please specify an action (or comma separated list of actions) via the ACTION environment variable"
+ end
+
+ actions = ENV['ACTION'].split(",")
+ actions.concat(ENV['PARAMS'].split(" ")) if ENV['PARAMS']
+
+ cap(*actions)
+ end
+end
+
+desc "Push the latest revision into production (delegates to remote:deploy)"
+task :deploy => "remote:deploy"
+
+desc "Rollback to the release before the current release in production (delegates to remote:rollback)"
+task :rollback => "remote:rollback"
diff --git a/lib/tasks/gems.rake b/lib/tasks/gems.rake
new file mode 100644
index 0000000..8c755be
--- /dev/null
+++ b/lib/tasks/gems.rake
@@ -0,0 +1,34 @@
+desc "Copy third-party gems into ./lib"
+task :freeze_other_gems do
+ # TODO Get this list from parsing environment.rb
+ libraries = %w(vpaypal)
+ require 'rubygems'
+ require 'find'
+
+ libraries.each do |library|
+ library_gem = Gem.cache.search(library).sort_by { |g| g.version }.last
+ puts "Freezing #{library} for #{library_gem.version}..."
+
+ # TODO Add dependencies to list of libraries to freeze
+ #library_gem.dependencies.each { |g| libraries << g }
+
+ folder_for_library = "#{library_gem.name}-#{library_gem.version}"
+ system "cd vendor; gem unpack -v '#{library_gem.version}' #{library_gem.name};"
+
+ # Copy files recursively to ./lib
+ folder_for_library_with_lib = "vendor/#{folder_for_library}/lib/"
+ Find.find(folder_for_library_with_lib) do |original_file|
+ destination_file = "./lib/" + original_file.gsub(folder_for_library_with_lib, '')
+
+ if File.directory?(original_file)
+ if !File.exist?(destination_file)
+ Dir.mkdir destination_file
+ end
+ else
+ File.copy original_file, destination_file
+ end
+ end
+
+ system "rm -r vendor/#{folder_for_library}"
+ end
+end
diff --git a/lib/uid.rb b/lib/uid.rb
new file mode 100644
index 0000000..9c3f6e9
--- /dev/null
+++ b/lib/uid.rb
@@ -0,0 +1,11 @@
+require 'securerandom'
+
+def uid()
+ chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ uid = ''
+ for i in 0..15 do
+ uid << chars[SecureRandom.random_number(chars.length), 1]
+ end
+ return uid
+end
+
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..0d9d5df
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,49 @@
+# Set default charset to unicode
+AddDefaultCharSet utf-8
+
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# If you don't want Rails to look in certain directories,
+# use the following rewrite rules so that Apache won't rewrite certain requests
+#
+# Example:
+# RewriteCond %{REQUEST_URI} ^/notrails.*
+# RewriteRule .* - [L]
+
+# Redirect all requests not available on the filesystem to Rails
+# By default the cgi dispatcher is used which is very slow
+#
+# For better performance replace the dispatcher with the fastcgi one
+#
+# Example:
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+
+RewriteEngine On
+
+# Let rails know that this is an https request so that it can redirect correctly
+#RequestHeader set X_FORWARDED_PROTO "https"
+
+# If your Rails application is accessed via an Alias directive,
+# then you MUST also set the RewriteBase in this htaccess file.
+#
+# Example:
+# Alias /myrailsapp /path/to/myrailsapp/public
+# RewriteBase /myrailsapp
+
+RewriteRule ^$ index.html [QSA]
+RewriteRule ^([^.]+)$ $1.html [QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
+
+# In case Rails experiences terminal errors
+# Instead of displaying this message you can supply a file here which will be rendered instead
+#
+# Example:
+# ErrorDocument 500 /500.html
+
+#ErrorDocument 500 "Application error Rails application failed to start properly"
+
+ErrorDocument 500 /500.html
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..424698f
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,52 @@
+
+
+
+
+ Page Not Found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 404 Not Found
+
+ What you are looking for is not here
+
+
+
+
+ <% if is_live?() && ! $STORE_PREFS['google_analytics_account'].blank? %>
+
+ <% end %>
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..bc2365d
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,52 @@
+
+
+
+
+ Application Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 500 Server Error
+
+ If only you had a nickel for everytime this happened...
+
+
+
+
+ <% if is_live?() && ! $STORE_PREFS['google_analytics_account'].blank? %>
+
+ <% end %>
+
+
diff --git a/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css b/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css
new file mode 100644
index 0000000..32b8761
--- /dev/null
+++ b/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css
@@ -0,0 +1 @@
+body{color:#222}h1{margin-top:40px}h2{margin:5px 0 10px;font-size:16px;border-bottom:1px dotted #ccc}#topmenu{margin:0;padding:0;padding-left:0}#topmenu li{height:40px;font-family:arial, "Trebuchet MS", "Lucida Grande", sans-serif;font-size:13px;padding-left:14px;padding-right:14px;list-style:none;display:table-cell !Important;vertical-align:bottom;border-right:1px dotted #ddd;border-left:1px dotted #f5f5f5;display:inline}#topmenu li:first-child{border-left:1px dotted #ddd}#submenu{margin:0;padding:0;padding-left:0}#submenu li{height:18px;font-family:arial, "Trebuchet MS", "Lucida Grande", sans-serif;font-size:13px;padding-left:14px;padding-right:14px;list-style:none;display:table-cell !Important;vertical-align:bottom;border-right:1px dotted #ddd;border-left:1px dotted #f5f5f5;display:inline}#submenu li:first-child{border-left:1px dotted #ddd}hr{height:20px;border:0}table.form tr td:first-child{text-align:right}tr{vertical-align:top}#search form{margin:1px 0 0 0}.pagination{margin:20px 0}table.list{border-collapse:collapse}table.list th{border:1px dotted #bbb;background-color:#f5f5f5;padding:5px}table.list td{border:1px dotted #999;padding:5px}table.list tr.F,tr.F A{color:#999}table.list tr.X{background-color:#e5e5e5}table.list tr.X td{border:1px dotted #aaa}table.list tr.R{background-color:#ccc}table.list tr.R td{border:1px dotted #aaa}table.list .address{width:150px}table.list tr.newday{border-top:3px double #ccc}.fl{float:left}.fr{float:right}.sosumi{font-size:10px;color:#555}.red{color:red}.bold{font-weight:bold}
diff --git a/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css.gz b/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css.gz
new file mode 100644
index 0000000..286c198
Binary files /dev/null and b/public/assets/admin-5b8adee5e7ac0fc20c5f98f1dcdc4063.css.gz differ
diff --git a/public/assets/admin.css b/public/assets/admin.css
new file mode 100644
index 0000000..32b8761
--- /dev/null
+++ b/public/assets/admin.css
@@ -0,0 +1 @@
+body{color:#222}h1{margin-top:40px}h2{margin:5px 0 10px;font-size:16px;border-bottom:1px dotted #ccc}#topmenu{margin:0;padding:0;padding-left:0}#topmenu li{height:40px;font-family:arial, "Trebuchet MS", "Lucida Grande", sans-serif;font-size:13px;padding-left:14px;padding-right:14px;list-style:none;display:table-cell !Important;vertical-align:bottom;border-right:1px dotted #ddd;border-left:1px dotted #f5f5f5;display:inline}#topmenu li:first-child{border-left:1px dotted #ddd}#submenu{margin:0;padding:0;padding-left:0}#submenu li{height:18px;font-family:arial, "Trebuchet MS", "Lucida Grande", sans-serif;font-size:13px;padding-left:14px;padding-right:14px;list-style:none;display:table-cell !Important;vertical-align:bottom;border-right:1px dotted #ddd;border-left:1px dotted #f5f5f5;display:inline}#submenu li:first-child{border-left:1px dotted #ddd}hr{height:20px;border:0}table.form tr td:first-child{text-align:right}tr{vertical-align:top}#search form{margin:1px 0 0 0}.pagination{margin:20px 0}table.list{border-collapse:collapse}table.list th{border:1px dotted #bbb;background-color:#f5f5f5;padding:5px}table.list td{border:1px dotted #999;padding:5px}table.list tr.F,tr.F A{color:#999}table.list tr.X{background-color:#e5e5e5}table.list tr.X td{border:1px dotted #aaa}table.list tr.R{background-color:#ccc}table.list tr.R td{border:1px dotted #aaa}table.list .address{width:150px}table.list tr.newday{border-top:3px double #ccc}.fl{float:left}.fr{float:right}.sosumi{font-size:10px;color:#555}.red{color:red}.bold{font-weight:bold}
diff --git a/public/assets/admin.css.gz b/public/assets/admin.css.gz
new file mode 100644
index 0000000..ac2cb2f
Binary files /dev/null and b/public/assets/admin.css.gz differ
diff --git a/public/assets/admin/coupons-95bd4fe1de99c1cd91ec8e6f348a44bd.css b/public/assets/admin/coupons-95bd4fe1de99c1cd91ec8e6f348a44bd.css
new file mode 100644
index 0000000..e69de29
diff --git a/public/assets/admin/coupons-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz b/public/assets/admin/coupons-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz
new file mode 100644
index 0000000..7c03a7a
Binary files /dev/null and b/public/assets/admin/coupons-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz differ
diff --git a/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js b/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js
new file mode 100644
index 0000000..1d62bdf
--- /dev/null
+++ b/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js
@@ -0,0 +1,3 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
+;
\ No newline at end of file
diff --git a/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js.gz b/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js.gz
new file mode 100644
index 0000000..239af08
Binary files /dev/null and b/public/assets/admin/coupons-eff3b777780f1289971eacf808d83488.js.gz differ
diff --git a/public/assets/admin/coupons.css b/public/assets/admin/coupons.css
new file mode 100644
index 0000000..e69de29
diff --git a/public/assets/admin/coupons.css.gz b/public/assets/admin/coupons.css.gz
new file mode 100644
index 0000000..59b8767
Binary files /dev/null and b/public/assets/admin/coupons.css.gz differ
diff --git a/public/assets/admin/coupons.js b/public/assets/admin/coupons.js
new file mode 100644
index 0000000..1d62bdf
--- /dev/null
+++ b/public/assets/admin/coupons.js
@@ -0,0 +1,3 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
+;
\ No newline at end of file
diff --git a/public/assets/admin/coupons.js.gz b/public/assets/admin/coupons.js.gz
new file mode 100644
index 0000000..6975c28
Binary files /dev/null and b/public/assets/admin/coupons.js.gz differ
diff --git a/public/assets/application-bf089660c407b5c0f9d69376cf745f41.js b/public/assets/application-bf089660c407b5c0f9d69376cf745f41.js
new file mode 100644
index 0000000..829f5e6
--- /dev/null
+++ b/public/assets/application-bf089660c407b5c0f9d69376cf745f41.js
@@ -0,0 +1,19 @@
+/*!
+ * jQuery JavaScript Library v1.7.1
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Nov 21 21:11:03 2011 -0500
+ */
+function correctPNG(){var a=navigator.appVersion.split("MSIE"),b=parseFloat(a[1]);if(b>=5.5&&document.body.filters)for(var c=0;c";d.outerHTML=j,c-=1}}}function setup_help_values(){for(element_id in HELP_VALUES)setup_help_value(element_id)}function setup_help_value(a){var b=$(a).get(0),c=HELP_VALUES[a];b.value==c?(b.value="",b.style.color="#000"):b.value==""&&(b.style.color="#aaa",b.value=c)}(function(a,b){function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c=0===c})}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function bj(a,b){if(b.nodeType!==1||!f.hasData(a))return;var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d0){if(c!=="border")for(;g").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(e.isReady)return;try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};return e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(a==="body"&&!d&&c.body)return this.context=c,this[0]=c.body,this.selector=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?g=[null,a,null]:g=i.exec(a);if(g&&(g[1]||!d)){if(g[1])return d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes),e.merge(this,a);h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}return this.context=c,this.selector=a,this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}return e.isFunction(a)?f.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),e.makeArray(a,this))},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();return e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return e.each(this,a,b)},ready:function(a){return e.bindReady(),A.add(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(A)return;A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}var b=i.call(arguments,0),c=0,d=b.length,e=new Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;ca ",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;return k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];if(!r)return;j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o=""+"",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i)}),b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?f.cache[a[f.expando]]:a[f.expando],!!a&&!m(a)},data:function(a,c,d,e){if(!f.acceptData(a))return;var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);return g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d),o&&!h[c]?g.events:(k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h,i)},removeData:function(a,b,c){if(!f.acceptData(a))return;var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];if(!arguments.length){if(g)return c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type],c&&"get"in c&&(d=c.get(g,"value"))!==b?d:(d=g.value,typeof d=="string"?d.replace(q,""):d==null?"":d);return}return e=f.isFunction(a),this.each(function(d){var g=f(this),h;if(this.nodeType!==1)return;e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!a||j===3||j===8||j===2)return;if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}return h&&"set"in h&&i&&(g=h.set(a,d,c))!==b?g:(a.setAttribute(c,""+d),d)}return h&&"get"in h&&i&&(g=h.get(a,c))!==null?g:(g=a.getAttribute(c),g===null?b:g)},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);return b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)")),b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))return;d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f=="undefined"||!!a&&f.event.triggered===a.type?b:f.event.dispatch.apply(i.elem,arguments)},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function w(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){return i=!1,0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length!==1||w[0]!=="~"&&w[0]!=="+"||!d.parentNode?d:d.parentNode,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);return l&&(m(l,h,e,f),m.uniqueSort(e)),e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);return a[0]=e++,a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");return!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" "),a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not"){if(!((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))){var g=m.filter(b[3],c,d,!0^f);return d||e.push.apply(e,g),!1}b[3]=m(b[3],null,null,c)}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){return a.unshift(!0),a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){return a=Array.prototype.slice.call(a,0),b?(b.push.apply(b,a),b):a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c ",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML=" ",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="
";if(b.querySelectorAll&&b.querySelectorAll(".TEST").length===0)return;m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!a.getElementsByClassName||a.getElementsByClassName("e").length===0)return;a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}return c=c.length>1?f.unique(c):c,this.pushStack(c,"closest",a)},index:function(a){return a?typeof a=="string"?f.inArray(this[0],f(a)):f.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);return L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse()),this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,""," "],thead:[1,""],tr:[2,""],td:[3,""],col:[2,""],area:[1,""," "],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div","
"]),f.fn.extend({text:function(a){return f.isFunction(a)?this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))}):typeof a!="object"&&a!==b?this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a)):f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return f.isFunction(a)?this.each(function(b){f(this).wrapInner(a.call(this,b))}):this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&
+this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);return a.push.apply(a,this.toArray()),this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);return a.push.apply(a,f.clean(arguments)),a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>$2>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}return d=e=null,h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;return f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight}),c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;return b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;return f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d)),f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/