diff --git a/Gemfile b/Gemfile index 7ecda1c3..ad6631ef 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ source 'http://rubygems.org' -gem 'spree_core', path: '~/spree/core' +# Remove after Spree 1.1.4 +gem 'spree_core', github: 'spree/spree', branch: '1-1-stable'#path: '~/spree/core' gemspec diff --git a/README.md b/README.md index 1b993950..50ff61de 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -SpreeGiftCard +SpreeGiftCard [![Build Status](https://secure.travis-ci.org/jdutil/spree_gift_card.png)](http://travis-ci.org/jdutil/spree_gift_card) [![Dependency Status](https://gemnasium.com/jdutil/spree_gift_card.png?travis)](https://gemnasium.com/jdutil/spree_gift_card) ============= This extension adds gift card functionality to spree. It is based off the original [spree_gift_cards](http://github.com/spree/spree_gift_cards) @@ -18,4 +18,9 @@ Testing 1. bundle exec rake test_app 1. bundle exec rspec spec +Todo +==== + +1. Have new gift card page mimic styling of product page + Copyright (c) 2012 Jeff Dutil, released under the New BSD License diff --git a/app/controllers/spree/checkout_controller_decorator.rb b/app/controllers/spree/checkout_controller_decorator.rb new file mode 100644 index 00000000..c841e931 --- /dev/null +++ b/app/controllers/spree/checkout_controller_decorator.rb @@ -0,0 +1,52 @@ +Spree::CheckoutController.class_eval do + + # TODO Apply gift code in a before filter if possible to avoid overriding the update method for easier upgrades? + def update + if @order.update_attributes(object_params) + fire_event('spree.checkout.update') + + if defined?(Spree::Promo) and @order.coupon_code.present? + event_name = "spree.checkout.coupon_code_added" + if Spree::Promotion.exists?(:code => @order.coupon_code, + :event_name => event_name) + + fire_event(event_name, :coupon_code => @order.coupon_code) + # If it doesn't exist, raise an error! + # Giving them another chance to enter a valid coupon code + else + flash[:error] = t(:promotion_not_found) + render :edit and return + end + end + + if @order.gift_code.present? + if gift_card = Spree::GiftCard.find_by_code(@order.gift_code) and gift_card.order_activatable?(@order) + fire_event('spree.checkout.gift_code_added', :gift_code => @order.gift_code) + gift_card.apply(@order) + else + flash[:error] = t(:gift_code_not_found) + render :edit and return + end + end + + if @order.next + state_callback(:after) + else + flash[:error] = t(:payment_processing_failed) + respond_with(@order, :location => checkout_state_path(@order.state)) + return + end + + if @order.state == 'complete' || @order.completed? + flash.notice = t(:order_processed_successfully) + flash[:commerce_tracking] = 'nothing special' + respond_with(@order, :location => completion_route) + else + respond_with(@order, :location => checkout_state_path(@order.state)) + end + else + respond_with(@order) { |format| format.html { render :edit } } + end + end + +end diff --git a/app/controllers/spree/gift_cards_controller.rb b/app/controllers/spree/gift_cards_controller.rb index 97741398..1d3c0ed6 100644 --- a/app/controllers/spree/gift_cards_controller.rb +++ b/app/controllers/spree/gift_cards_controller.rb @@ -10,8 +10,16 @@ def new 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) + # Create line item + line_item = LineItem.new(quantity: 1) + line_item.gift_card = @gift_card + line_item.variant = @gift_card.variant + line_item.price = @gift_card.variant.price + # Add to order + order = current_order(true) + order.line_items << line_item + order.save + # Save gift card @gift_card.line_item = line_item @gift_card.save redirect_to cart_path diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index 3cc931ea..91b270eb 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -1,5 +1,6 @@ Spree::OrdersController.class_eval do + # TODO Apply gift code in a before filter if possible to avoid overriding the update method for easier upgrades? def update @order = current_order if @order.update_attributes(params[:order]) @@ -30,13 +31,13 @@ def update 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 + return false if @order.gift_code.blank? + if gift_card = Spree::GiftCard.find_by_code(@order.gift_code) and gift_card.order_activatable?(@order) + fire_event('spree.checkout.gift_code_added', :gift_code => @order.gift_code) + gift_card.apply(@order) + return true + else + return false end end diff --git a/app/controllers/spree/products_controller_decorator.rb b/app/controllers/spree/products_controller_decorator.rb new file mode 100644 index 00000000..02812a48 --- /dev/null +++ b/app/controllers/spree/products_controller_decorator.rb @@ -0,0 +1,11 @@ +Spree::ProductsController.class_eval do + + before_filter :redirect_gift_card, only: :show + + private + + def redirect_gift_card + redirect_to new_gift_card_path and return false if @product.is_gift_card? + end + +end diff --git a/app/models/spree.rb b/app/models/spree.rb new file mode 100644 index 00000000..11e0699b --- /dev/null +++ b/app/models/spree.rb @@ -0,0 +1,5 @@ +module Spree + def self.table_name_prefix + 'spree_' + end +end diff --git a/app/models/spree/calculator/gift_card.rb b/app/models/spree/calculator/gift_card.rb new file mode 100644 index 00000000..da66f2fa --- /dev/null +++ b/app/models/spree/calculator/gift_card.rb @@ -0,0 +1,13 @@ +module Spree + class Calculator::GiftCard < Calculator + + def self.description + 'Gift Card Calculator' + end + + def compute(order, gift_card) + # Ensure a negative amount which does not exceed the sum of the order's item_total, ship_total, and tax_total. + [(order.item_total + order.ship_total + order.tax_total), gift_card.current_value].min * -1 + end + end +end diff --git a/app/models/spree/gift_card.rb b/app/models/spree/gift_card.rb index 61c5e704..0802a5ae 100644 --- a/app/models/spree/gift_card.rb +++ b/app/models/spree/gift_card.rb @@ -5,31 +5,36 @@ class GiftCard < ActiveRecord::Base UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"] + attr_accessible :email, :name, :note, :variant_id + belongs_to :variant belongs_to :line_item + has_many :transactions, class_name: 'Spree::GiftCardTransaction' + + validates :code, presence: true, uniqueness: true 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 :generate_code, on: :create + before_validation :set_calculator, 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 + + # Calculate the amount to be used when creating an adjustment + def compute_amount(calculable) + self.calculator.compute(calculable, self) end def price @@ -44,14 +49,14 @@ def order_activatable?(order) 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) + def generate_code + until self.code.present? && self.class.where(code: self.code).count == 0 + self.code = Digest::SHA1.hexdigest([Time.now, rand].join) end end def set_calculator - self.calculator = Spree::Calculator::FlatRate.new({preferred_amount: -(self.current_value || 0)}) + self.calculator = Spree::Calculator::GiftCard.new end def set_values diff --git a/app/models/spree/gift_card_transaction.rb b/app/models/spree/gift_card_transaction.rb new file mode 100644 index 00000000..e4d9bdea --- /dev/null +++ b/app/models/spree/gift_card_transaction.rb @@ -0,0 +1,8 @@ +class Spree::GiftCardTransaction < ActiveRecord::Base + belongs_to :gift_card + belongs_to :order + + validates :amount, presence: true + validates :gift_card, presence: true + validates :order, presence: true +end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb new file mode 100644 index 00000000..83141f99 --- /dev/null +++ b/app/models/spree/line_item_decorator.rb @@ -0,0 +1,7 @@ +Spree::LineItem.class_eval do + + has_one :gift_card + + validates :gift_card, presence: { if: Proc.new{ |item| item.product.is_gift_card? } } + +end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index f12437bc..03532033 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -9,32 +9,28 @@ 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 + # Called after transition to complete state when payments will have been processed. def finalize_with_gift_card! finalize_without_gift_card! + # Send out emails for any newly purchased gift cards. self.line_items.each do |li| Spree::OrderMailer.gift_card_email(li.gift_card, self).deliver if li.gift_card end + # Record any gift card redemptions. + self.adjustments.where(originator_type: 'Spree::GiftCard').each do |adjustment| + gift_card_transaction = adjustment.originator.transactions.build + gift_card_transaction.amount = adjustment.amount + gift_card_transaction.order = self + gift_card_transaction.save + end end alias_method_chain :finalize!, :gift_card + # If variant is a gift card we say order doesn't already contain it so that each gift card is it's own line item. def contains?(variant) return false if variant.product.is_gift_card? - line_items.detect{ |line_item| line_item.variant_id == variant.id } + line_items.detect { |line_item| line_item.variant_id == variant.id } end end diff --git a/app/overrides/spree/checkout/_payment/_gift_code_field.html.erb.deface b/app/overrides/spree/checkout/_payment/_gift_code_field.html.erb.deface new file mode 100644 index 00000000..dbcdb819 --- /dev/null +++ b/app/overrides/spree/checkout/_payment/_gift_code_field.html.erb.deface @@ -0,0 +1,7 @@ + + +

+ <%= form.label :gift_code %>
+ <%= form.text_field :gift_code, value: nil %> +

diff --git a/app/views/spree/order_mailer/gift_card_email.html.erb b/app/views/spree/order_mailer/gift_card_email.html.erb index 7ecbe85c..0bb0c783 100644 --- a/app/views/spree/order_mailer/gift_card_email.html.erb +++ b/app/views/spree/order_mailer/gift_card_email.html.erb @@ -1,3 +1,3 @@

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

+

To use your <%= number_to_currency @gift_card.price %> Gift Card enter the following Gift Code during checkout: <%= @gift_card.code %>

diff --git a/db/migrate/20101011103850_create_spree_gift_cards.rb b/db/migrate/20101011103850_create_spree_gift_cards.rb index ffd47faf..b14f74ea 100644 --- a/db/migrate/20101011103850_create_spree_gift_cards.rb +++ b/db/migrate/20101011103850_create_spree_gift_cards.rb @@ -6,7 +6,7 @@ def change t.string :email, :null => false t.string :name t.text :note - t.string :token, :null => false + t.string :code, :null => false t.boolean :is_received, :default => false, :null => false t.datetime :sent_at t.decimal :current_value, :precision => 8, :scale => 2, :null => false diff --git a/db/migrate/20121017183422_create_spree_gift_card_transactions.rb b/db/migrate/20121017183422_create_spree_gift_card_transactions.rb new file mode 100644 index 00000000..8d392d58 --- /dev/null +++ b/db/migrate/20121017183422_create_spree_gift_card_transactions.rb @@ -0,0 +1,10 @@ +class CreateSpreeGiftCardTransactions < ActiveRecord::Migration + def change + create_table :spree_gift_card_transactions do |t| + t.decimal :amount, scale: 2 + t.belongs_to :gift_card + t.belongs_to :order + t.timestamps + end + end +end diff --git a/lib/spree_gift_card/engine.rb b/lib/spree_gift_card/engine.rb index f9dbe45d..bd170264 100644 --- a/lib/spree_gift_card/engine.rb +++ b/lib/spree_gift_card/engine.rb @@ -15,8 +15,6 @@ def self.activate Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c| Rails.configuration.cache_classes ? require(c) : load(c) end - Spree::User.has_many :gift_cards - Spree::LineItem.has_one :gift_card end config.to_prepare &method(:activate).to_proc diff --git a/script/rails b/script/rails new file mode 100644 index 00000000..134d6989 --- /dev/null +++ b/script/rails @@ -0,0 +1,7 @@ +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +ENGINE_ROOT = File.expand_path('../..', __FILE__) +ENGINE_PATH = File.expand_path('../../lib/spree_gift_card/engine', __FILE__) + +require 'rails/all' +require 'rails/engine/commands' diff --git a/spec/models/spree/gift_card_spec.rb b/spec/models/spree/gift_card_spec.rb index 51b0ae08..9d6fa874 100644 --- a/spec/models/spree/gift_card_spec.rb +++ b/spec/models/spree/gift_card_spec.rb @@ -2,28 +2,62 @@ describe Spree::GiftCard do + it {should have_many(:transactions)} + it {should validate_presence_of(:current_value)} it {should validate_presence_of(:email)} it {should validate_presence_of(:original_value)} it {should validate_presence_of(:name)} - it "should generate token before create" do + it "should generate code before create" do card = Spree::GiftCard.create(:email => "test@mail.com", :name => "John", :variant_id => create(:variant).id) - card.token.should_not == nil + card.code.should_not be_nil end it "should set current_value and original_value before create" do card = Spree::GiftCard.create(:email => "test@mail.com", :name => "John", :variant_id => create(:variant).id) - card.current_value.should_not == nil - card.original_value.should_not == nil + card.current_value.should_not be_nil + card.original_value.should_not be_nil + end + + it 'should only have certain attributes be accessible' do + subject.class.accessible_attributes.to_a.should eql([ + '', # WTF? no idea why a blank value is being added... + 'email', + 'name', + 'note', + 'variant_id', + 'calculator_type', + 'calculator_attributes' + ]) end - it "should not allow user set line_item_id and user_id" do - lambda { - Spree::GiftCard.create(:email => "test@mail.com", :name => "John", :variant_id => create(:variant).id, :line_item_id => 1) - }.should raise_error(ActiveModel::MassAssignmentSecurity::Error, /line_item_id/) - lambda { - Spree::GiftCard.create(:email => "test@mail.com", :name => "John", :variant_id => create(:variant).id, :user_id => 1) - }.should raise_error(ActiveModel::MassAssignmentSecurity::Error, /user_id/) + context '#apply' do + let(:gift_card) { create(:gift_card, variant: create(:variant, price: 25)) } + + context 'for order total larger than gift card amount' do + it 'creates adjustment for full amount' do + order = create(:order_with_totals) + create(:line_item, order: order, price: 75, variant: create(:variant, price: 75)) + order.reload # reload so line item is associated + order.update! + gift_card.apply(order) + order.adjustments.find_by_originator_id_and_originator_type(gift_card.id, gift_card.class.to_s).amount.to_f.should eql(-25.0) + end + end + + context 'for order total smaller than gift card amount' do + it 'creates adjustment for order total' do + order = create(:order_with_totals) + order.reload # reload so line item is associated + order.update! # update so order calculates totals + gift_card.apply(order) + # default line item is priced at 10 + order.adjustments.find_by_originator_id_and_originator_type(gift_card.id, gift_card.class.to_s).amount.to_f.should eql(-10.0) + + pending 'test should also have tax & shipping...' + end + end end + end diff --git a/spec/models/spree/gift_card_transaction_spec.rb b/spec/models/spree/gift_card_transaction_spec.rb new file mode 100644 index 00000000..5de3e793 --- /dev/null +++ b/spec/models/spree/gift_card_transaction_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Spree::GiftCardTransaction do + it {should belong_to(:gift_card)} + it {should belong_to(:order)} + it {should validate_presence_of(:amount)} + it {should validate_presence_of(:gift_card)} + it {should validate_presence_of(:order)} +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb new file mode 100644 index 00000000..8be960a6 --- /dev/null +++ b/spec/models/spree/order_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Spree::Order do + + it '#contains? should return false if variant is gift card' do + pending + end + + context '#finalize!' do + + let(:gift_card) { create(:gift_card, variant: create(:variant, price: 25)) } + + context 'when redeeming gift card' do + it 'creates transactions' do + order = create(:order_with_totals) + order.line_items = [create(:line_item, order: order, price: 75, variant: create(:variant, price: 75))] + order.reload # reload so line item is associated + order.update! + gift_card.apply(order) + order.finalize! + transaction = gift_card.transactions.first + transaction.amount.to_f.should eql(-25.0) + transaction.gift_card.should eql(gift_card) + transaction.order.should eql(order) + end + end + + context 'when purchasing gift card' do + it 'sends emails' do + order = create(:order_with_totals) + order.line_items = [create(:line_item, gift_card: gift_card, order: order, price: 25, variant: gift_card.variant)] + order.reload # reload so line item is associated + order.update! + Spree::OrderMailer.stub_chain(:gift_card_email, :deliver).and_return(true) + Spree::OrderMailer.should_receive(:gift_card_email).with(gift_card, order).once + order.finalize! + end + end + + end + +end diff --git a/spec/requests/checkout_spec.rb b/spec/requests/checkout_spec.rb index 0879419f..984b9eba 100644 --- a/spec/requests/checkout_spec.rb +++ b/spec/requests/checkout_spec.rb @@ -3,14 +3,15 @@ describe "Checkout" do before do - @product = create(:product, :name => "RoR Mug") - create(:zone) - create(:shipping_method) + create(:gift_card, code: "foobar", variant: create(:variant, price: 25)) + country = create(:country, name: "United States") + create(:state, name: "Alaska", country: country) + zone = create(:zone, zone_members: [Spree::ZoneMember.create(zoneable: country)]) + create(:shipping_method, zone: zone) create(:payment_method) + create(:product, name: "RoR Mug", price: 30) end - let!(:gift_card) { create(:gift_card, :token => "onetwo") } - # Wait for Spree 1.2.x when promos & adjustments are also handled on the cart page. # context "on the cart page" do # before do @@ -39,8 +40,10 @@ visit spree.root_path click_link "RoR Mug" click_button "add-to-cart-button" - click_link "Checkout" + + # TODO not sure why registration page is ignored so just update order here. + Spree::Order.last.update_column(:email, "spree@example.com") # fill_in "order_email", :with => "spree@example.com" # click_button "Continue" @@ -59,10 +62,45 @@ # To payment screen click_button "Save and Continue" - fill_in "Gift Code", :with => "coupon_codes_rule_man" + fill_in "Gift code", :with => "coupon_codes_rule_man" click_button "Save and Continue" page.should have_content("The gift code you entered doesn't exist. Please try again.") end + + it "displays valid gift code's adjustment", :js => true do + visit spree.root_path + click_link "RoR Mug" + click_button "add-to-cart-button" + click_link "Checkout" + + # TODO not sure why registration page is ignored so just update order here. + Spree::Order.last.update_column(:email, "spree@example.com") + # fill_in "order_email", :with => "spree@example.com" + # click_button "Continue" + + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Smith" + fill_in "Street Address", :with => "1 John Street" + fill_in "City", :with => "City of John" + fill_in "Zip", :with => "01337" + select "United States", :from => "Country" + select "Alaska", :from => "order[bill_address_attributes][state_id]" + fill_in "Phone", :with => "555-555-5555" + check "Use Billing Address" + + # To shipping method screen + click_button "Save and Continue" + # To payment screen + click_button "Save and Continue" + + fill_in "Gift code", :with => "foobar" + click_button "Save and Continue" + within "[data-hook='order_details_adjustments']" do + page.should have_content("Gift Card") + page.should have_content("-$25.00") + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 04826ef5..c96139ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,39 +31,22 @@ RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods - - # == URL Helpers - # - # Allows access to Spree's routes in specs: - # - # visit spree.admin_path - # current_path.should eql(spree.products_path) config.include Spree::Core::UrlHelpers - - # == Mock Framework - # - # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: - # - # config.mock_with :mocha - # config.mock_with :flexmock - # config.mock_with :rr config.mock_with :rspec - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. + # Set to false for running JS drivers. config.use_transactional_fixtures = false - config.before(:each) do + config.before :each do if example.metadata[:js] - DatabaseCleaner.strategy = :truncation, { :except => ['spree_countries', 'spree_zones', 'spree_zone_members', 'spree_states', 'spree_roles'] } + DatabaseCleaner.strategy = :truncation else DatabaseCleaner.strategy = :transaction end DatabaseCleaner.start end - config.after(:each) do + config.after :each do DatabaseCleaner.clean end diff --git a/spree_gift_card.gemspec b/spree_gift_card.gemspec index 4d4365fe..c6a0194c 100644 --- a/spree_gift_card.gemspec +++ b/spree_gift_card.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'capybara', '~> 1.1' s.add_development_dependency 'coffee-rails' s.add_development_dependency 'database_cleaner' - s.add_development_dependency 'factory_girl', '~> 3.6'# '~> 4.1' # cant use 4.1 until Spree's factories are compatible probably in 1.2.x + s.add_development_dependency 'factory_girl', '~> 3.6'# '~> 4.1' # cant use 4.1 until Spree's factories are compatible maybe in 1.2.x s.add_development_dependency 'ffaker' s.add_development_dependency 'rspec-rails', '~> 2.11' s.add_development_dependency 'sass-rails'