diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9c0690ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +\#* +*~ +.#* +.DS_Store +.idea +.project +tmp +nbproject +*.swp +spec/test_app +spec/dummy +Gemfile.lock +coverage +.sass-cache diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..7ecda1c3 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'http://rubygems.org' +gem 'spree_core', path: '~/spree/core' +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..58006386 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Rails Dog LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..1b993950 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +SpreeGiftCard +============= + +This extension adds gift card functionality to spree. It is based off the original [spree_gift_cards](http://github.com/spree/spree_gift_cards) +extension, but differs in that it does not require a user to have an account. Gift cards may be redeemed by +entering a unique gift card code during checkout rather than applying store credits to the customers account. + +Installation +============ + +1. Add `spree_gift_card` to Gemfile +1. Run `rails g spree_gift_card:install` +1. Run `rails g spree_gift_card:seed` + +Testing +======= + +1. bundle exec rake test_app +1. bundle exec rspec spec + +Copyright (c) 2012 Jeff Dutil, released under the New BSD License diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..8cbc541c --- /dev/null +++ b/Rakefile @@ -0,0 +1,15 @@ +require 'bundler' +Bundler::GemHelper.install_tasks + +require 'rspec/core/rake_task' +require 'spree/core/testing_support/common_rake' + +RSpec::Core::RakeTask.new + +task :default => [:spec] + +desc 'Generates a dummy app for testing' +task :test_app do + ENV['LIB_NAME'] = 'spree_gift_card' + Rake::Task['common:test_app'].invoke +end diff --git a/Versionfile b/Versionfile new file mode 100644 index 00000000..980b5fbe --- /dev/null +++ b/Versionfile @@ -0,0 +1 @@ +"1.1.x" => { :branch => "master" } diff --git a/app/assets/javascripts/admin/spree_gift_card.js b/app/assets/javascripts/admin/spree_gift_card.js new file mode 100644 index 00000000..a3b2c532 --- /dev/null +++ b/app/assets/javascripts/admin/spree_gift_card.js @@ -0,0 +1 @@ +//= require admin/spree_core diff --git a/app/assets/javascripts/store/spree_gift_card.js b/app/assets/javascripts/store/spree_gift_card.js new file mode 100644 index 00000000..d5cb5c75 --- /dev/null +++ b/app/assets/javascripts/store/spree_gift_card.js @@ -0,0 +1 @@ +//= require store/spree_core diff --git a/app/assets/stylesheets/admin/spree_gift_card.css b/app/assets/stylesheets/admin/spree_gift_card.css new file mode 100644 index 00000000..21ef02a6 --- /dev/null +++ b/app/assets/stylesheets/admin/spree_gift_card.css @@ -0,0 +1,3 @@ +/* + *= require admin/spree_core +*/ diff --git a/app/assets/stylesheets/store/spree_gift_card.css b/app/assets/stylesheets/store/spree_gift_card.css new file mode 100644 index 00000000..94dbe33a --- /dev/null +++ b/app/assets/stylesheets/store/spree_gift_card.css @@ -0,0 +1,3 @@ +/* + *= require store/spree_core +*/ diff --git a/app/controllers/spree/gift_cards_controller.rb b/app/controllers/spree/gift_cards_controller.rb new file mode 100644 index 00000000..97741398 --- /dev/null +++ b/app/controllers/spree/gift_cards_controller.rb @@ -0,0 +1,32 @@ +module Spree + class GiftCardsController < Spree::BaseController + helper 'spree/admin/base' + + def new + find_gift_card_variants + @gift_card = GiftCard.new + end + + def create + @gift_card = GiftCard.new(params[:gift_card]) + if @gift_card.save + @order = current_order(true) + line_item = @order.add_variant(@gift_card.variant, 1) + @gift_card.line_item = line_item + @gift_card.save + redirect_to cart_path + else + find_gift_card_variants + render :action => :new + end + end + + private + + def find_gift_card_variants + gift_card_product_ids = Product.not_deleted.where(["is_gift_card = ?", true]).map(&:id) + @gift_card_variants = Variant.where(["price > 0 AND product_id IN (?)", gift_card_product_ids]).order("price") + end + + end +end diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb new file mode 100644 index 00000000..3cc931ea --- /dev/null +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -0,0 +1,43 @@ +Spree::OrdersController.class_eval do + + def update + @order = current_order + if @order.update_attributes(params[:order]) + if defined?(Spree::Promo) and @order.coupon_code.present? + if apply_coupon_code + flash[:notice] = t(:coupon_code_applied) + else + flash[:error] = t(:promotion_not_found) + render :edit and return + end + end + if @order.gift_code.present? + if apply_gift_code + flash[:notice] = t(:gift_code_applied) + else + flash[:error] = t(:gift_code_not_found) + render :edit and return + end + end + @order.line_items = @order.line_items.select { |li| li.quantity > 0 } + fire_event('spree.order.contents_changed') + respond_with(@order) { |format| format.html { redirect_to cart_path } } + else + respond_with(@order) + end + end + + private + + def apply_gift_code + return if @order.gift_code.blank? + if gift = Spree::GiftCard.find_by_token(@order.gift_code) + if gift.order_activatable?(@order) + fire_event('spree.checkout.gift_code_added', :gift_code => @order.gift_code) + gift.apply(@order) + true + end + end + end + +end diff --git a/app/mailers/spree/order_mailer_decorator.rb b/app/mailers/spree/order_mailer_decorator.rb new file mode 100644 index 00000000..58a97b55 --- /dev/null +++ b/app/mailers/spree/order_mailer_decorator.rb @@ -0,0 +1,9 @@ +Spree::OrderMailer.class_eval do + def gift_card_email(card, order) + @gift_card = card + @order = order + subject = "#{Spree::Config[:site_name]} Gift Card" + @gift_card.update_attribute(:sent_at, Time.now) + mail(:to => card.email, :subject => subject) + end +end diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb new file mode 100644 index 00000000..408d8804 --- /dev/null +++ b/app/models/spree/adjustment_decorator.rb @@ -0,0 +1,5 @@ +Spree::Adjustment.class_eval do + + scope :gift_card, where(:originator_type => 'Spree::GiftCard') + +end diff --git a/app/models/spree/gift_card.rb b/app/models/spree/gift_card.rb new file mode 100644 index 00000000..61c5e704 --- /dev/null +++ b/app/models/spree/gift_card.rb @@ -0,0 +1,63 @@ +require 'spree/core/validators/email' + +module Spree + class GiftCard < ActiveRecord::Base + + UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"] + + belongs_to :variant + belongs_to :line_item + + validates :current_value, presence: true + validates :email, email: true, presence: true + validates :original_value, presence: true + validates :name, presence: true + validates :token, presence: true, uniqueness: true + + before_validation :generate_token, on: :create + before_validation :set_values, on: :create + before_validation :set_calculator # Goes after set_values to ensure current_value is set. + + attr_accessible :email, :name, :note, :variant_id + + calculated_adjustments + + def apply(order) + # Nothing to do if the gift card is already associated with the order + return if order.gift_credit_exists?(self) + # order.adjustments.gift_card.reload.clear + order.update! + create_adjustment(I18n.t(:gift_card), order, order) + order.update! + # TODO: if successful we should update preferred amount or should that be done elsewhere? Might make sense to create a new calculator that does the updating + end + + def price + self.line_item ? self.line_item.price * self.line_item.quantity : self.variant.price + end + + def order_activatable?(order) + order && + created_at.to_i < order.created_at.to_i && + !UNACTIVATABLE_ORDER_STATES.include?(order.state) + end + + private + + def generate_token + until self.token.present? && self.class.where(token: self.token).count == 0 + self.token = Digest::SHA1.hexdigest([Time.now, rand].join) + end + end + + def set_calculator + self.calculator = Spree::Calculator::FlatRate.new({preferred_amount: -(self.current_value || 0)}) + end + + def set_values + self.current_value = self.variant.try(:price) + self.original_value = self.variant.try(:price) + end + + end +end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb new file mode 100644 index 00000000..f12437bc --- /dev/null +++ b/app/models/spree/order_decorator.rb @@ -0,0 +1,40 @@ +Spree::Order.class_eval do + + attr_accessible :gift_code + attr_accessor :gift_code + + # Tells us if there is the specified gift code already associated with the order + # regardless of whether or not its currently eligible. + def gift_credit_exists?(gift_card) + !! adjustments.gift_card.reload.detect { |credit| credit.originator_id == gift_card.id } + end + + # unless self.method_defined?('update_adjustments_with_promotion_limiting') + # def update_adjustments_with_promotion_limiting + # update_adjustments_without_promotion_limiting + # return if adjustments.promotion.eligible.none? + # most_valuable_adjustment = adjustments.promotion.eligible.max{|a,b| a.amount.abs <=> b.amount.abs} + # current_adjustments = (adjustments.promotion.eligible - [most_valuable_adjustment]) + # current_adjustments.each do |adjustment| + # adjustment.update_attribute_without_callbacks(:eligible, false) + # end + # end + # alias_method_chain :update_adjustments, :promotion_limiting + # end + + # Finalizes an in progress order after checkout is complete. + # Called after transition to complete state when payments will have been processed + def finalize_with_gift_card! + finalize_without_gift_card! + self.line_items.each do |li| + Spree::OrderMailer.gift_card_email(li.gift_card, self).deliver if li.gift_card + end + end + alias_method_chain :finalize!, :gift_card + + def contains?(variant) + return false if variant.product.is_gift_card? + line_items.detect{ |line_item| line_item.variant_id == variant.id } + end + +end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb new file mode 100644 index 00000000..e8f60357 --- /dev/null +++ b/app/models/spree/product_decorator.rb @@ -0,0 +1,8 @@ +Spree::Product.class_eval do + + attr_accessible :is_gift_card + + scope :gift_cards, where(is_gift_card: true) + scope :not_gift_cards, where(is_gift_card: false) + +end diff --git a/app/overrides/insert_bottom_admin_product_form_right.rb b/app/overrides/insert_bottom_admin_product_form_right.rb new file mode 100644 index 00000000..3cfa83fd --- /dev/null +++ b/app/overrides/insert_bottom_admin_product_form_right.rb @@ -0,0 +1,5 @@ +Deface::Override.new(:virtual_path => "spree/admin/products/_form", + :name => "insert_bottom_admin_product_form_right", + :insert_bottom => "[data-hook='admin_product_form_right']", + :partial => %q{spree/admin/products/gift_card_fields}, + :disabled => false) diff --git a/app/overrides/insert_bottom_order_item_description.rb b/app/overrides/insert_bottom_order_item_description.rb new file mode 100644 index 00000000..cbfb0ed9 --- /dev/null +++ b/app/overrides/insert_bottom_order_item_description.rb @@ -0,0 +1,5 @@ +Deface::Override.new(:virtual_path => "spree/shared/_order_details", + :name => "insert_bottom_order_item_description", + :insert_bottom => "[data-hook='order_item_description']", + :partial => 'spree/orders/item_gift_certificate_info', + :disabled => false) diff --git a/app/overrides/insert_bottom_sidebar.rb b/app/overrides/insert_bottom_sidebar.rb new file mode 100644 index 00000000..eb9e7825 --- /dev/null +++ b/app/overrides/insert_bottom_sidebar.rb @@ -0,0 +1,6 @@ +Deface::Override.new(:virtual_path => "spree/layouts/spree_application", + :name => "insert_bottom_sidebar", + :insert_bottom => "#sidebar, [data-hook='sidebar']", + :text => %q{<%= link_to t("buy_gift_card"), new_gift_card_path, :class => 'button' %>}, + :disabled => false, + :original => '2f11ce271ae3b346b9fc6a927598ad6d6d6a1885') diff --git a/app/overrides/replace_contents_cart_item_description.rb b/app/overrides/replace_contents_cart_item_description.rb new file mode 100644 index 00000000..24d52c7e --- /dev/null +++ b/app/overrides/replace_contents_cart_item_description.rb @@ -0,0 +1,6 @@ +Deface::Override.new(:virtual_path => "spree/orders/_line_item", + :name => "replace_contents_cart_item_description", + :insert_bottom => "[data-hook='cart_item_description']", + :partial => 'spree/orders/line_item_gift_certificate_info', + :disabled => false, + :original => '95a090c709b76195844a3e0019062916e7595109') diff --git a/app/views/spree/admin/products/_gift_card_fields.html.erb b/app/views/spree/admin/products/_gift_card_fields.html.erb new file mode 100644 index 00000000..cdff852c --- /dev/null +++ b/app/views/spree/admin/products/_gift_card_fields.html.erb @@ -0,0 +1,3 @@ +
+ <%= f.check_box(:is_gift_card) %> <%= f.label :is_gift_card, t("is_gift_card")%> +
diff --git a/app/views/spree/gift_cards/new.html.erb b/app/views/spree/gift_cards/new.html.erb new file mode 100644 index 00000000..b8c08de4 --- /dev/null +++ b/app/views/spree/gift_cards/new.html.erb @@ -0,0 +1,22 @@ ++ <% @gift_card_variants.each do |card| %> + <%= f.radio_button :variant_id, card.id %> <%= number_to_currency(card.price) %> + <% end %> +
+ <%= f.field_container :email do %> + <%= f.label :email, t("email") %> *Hi <%= @gift_card.name %>,
+<%= @gift_card.note %>
+To use your <%= number_to_currency @gift_card.price %> Gift Card enter the following Gift Code during checkout: <%= @gift_card.token %>
diff --git a/app/views/spree/orders/_gift_code_field.html.erb b/app/views/spree/orders/_gift_code_field.html.erb new file mode 100644 index 00000000..981de41c --- /dev/null +++ b/app/views/spree/orders/_gift_code_field.html.erb @@ -0,0 +1,8 @@ + +