From fa7eceec9b41bec5e978920346d0909bb3c78365 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 12 Nov 2015 05:51:25 -0600 Subject: [PATCH] * wip --- admin/app/admin/gift_cards.rb | 41 ++++++ admin/config/initializers/gift_cards.rb | 9 ++ db/up/giftcard.sql | 6 +- ruby/lib/jam_ruby.rb | 1 + .../jam_ruby/constants/validation_messages.rb | 2 + ruby/lib/jam_ruby/models/anonymous_user.rb | 6 + ruby/lib/jam_ruby/models/gift_card.rb | 27 ++-- ruby/lib/jam_ruby/models/sale.rb | 24 ++- ruby/lib/jam_ruby/models/shopping_cart.rb | 55 +++++++ ruby/lib/jam_ruby/models/user.rb | 29 +++- ruby/spec/factories.rb | 5 + ruby/spec/jam_ruby/models/sale_spec.rb | 107 ++++++++++++++ .../jam_ruby/models/shopping_cart_spec.rb | 62 ++++---- web/app/assets/javascripts/jam_rest.js | 16 +- .../landing/RedeemGiftCardPage.js.jsx.coffee | 137 +++++++++++++++++- .../stylesheets/client/jamkazam.css.scss | 21 ++- .../react-components/SessionScreen.css.scss | 19 --- .../landings/redeem_giftcard.css.scss | 42 +++++- web/app/controllers/api_recurly_controller.rb | 7 + web/app/controllers/api_users_controller.rb | 44 +++++- .../views/api_shopping_carts/remove_cart.rabl | 2 +- web/app/views/clients/index.html.erb | 1 - web/config/routes.rb | 3 + web/lib/user_manager.rb | 4 +- .../api_shopping_carts_controller_spec.rb | 13 +- .../controllers/api_users_controller_spec.rb | 122 ++++++++++++++++ web/spec/factories.rb | 5 + web/spec/features/redeem_giftcard_spec.rb | 35 +++++ web/spec/managers/user_manager_spec.rb | 73 ++++++++++ 29 files changed, 827 insertions(+), 91 deletions(-) create mode 100644 admin/app/admin/gift_cards.rb create mode 100644 admin/config/initializers/gift_cards.rb create mode 100644 web/spec/features/redeem_giftcard_spec.rb diff --git a/admin/app/admin/gift_cards.rb b/admin/app/admin/gift_cards.rb new file mode 100644 index 000000000..ab45bdab2 --- /dev/null +++ b/admin/app/admin/gift_cards.rb @@ -0,0 +1,41 @@ +ActiveAdmin.register_page "Giftcards" do + + menu :label => 'Gift Cards', :parent => 'JamTracks' + + page_action :upload_giftcards, :method => :post do + GiftCard.transaction do + + puts params + + file = params[:jam_ruby_gift_card][:csv] + array_of_arrays = CSV.read(file.tempfile.path) + array_of_arrays.each do |row| + if row.length != 1 + raise "UKNONWN CSV FORMAT! Must be 1 column" + end + + code = row[0] + + gift_card = GiftCard.new + gift_card.code = code + gift_card.card_type = params[:jam_ruby_gift_card][:card_type] + gift_card.origin = file .original_filename + gift_card.save! + end + + redirect_to admin_giftcards_path, :notice => "Created #{array_of_arrays.length} gift cards!" + end + end + + content do + semantic_form_for GiftCard.new, :url => admin_giftcards_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Upload Gift Cards" do + f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)" + f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES + end + f.actions + end + end + +end + diff --git a/admin/config/initializers/gift_cards.rb b/admin/config/initializers/gift_cards.rb new file mode 100644 index 000000000..8c967ccb4 --- /dev/null +++ b/admin/config/initializers/gift_cards.rb @@ -0,0 +1,9 @@ +class JamRuby::GiftCard + + attr_accessor :csv + + + def process_csv + + end +end diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql index f4d6b32ba..eae33c13e 100644 --- a/db/up/giftcard.sql +++ b/db/up/giftcard.sql @@ -1,13 +1,13 @@ -CREATE TABLE gift_card ( +CREATE TABLE gift_cards ( id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), code VARCHAR(64) UNIQUE NOT NULL, user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE, card_type VARCHAR(64) NOT NULL, - used BOOLEAN NOT NULL DEFAULT FALSE, + origin VARCHAR(200), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX gift_card_user_id_idx ON gift_card(user_id); +CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id); ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0; diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 21e2446d9..1cbb0cdab 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -253,6 +253,7 @@ require "jam_ruby/models/musician_search" require "jam_ruby/models/band_search" require "jam_ruby/import/tency_stem_mapping" require "jam_ruby/models/jam_track_search" +require "jam_ruby/models/gift_card" include Jampb diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 662fce608..e84dff7c3 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -19,6 +19,8 @@ module ValidationMessages # sessions SESSION_NOT_FOUND = "Session not found." + NOT_FOUND = 'not found' + # genres RECORDING_GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed." BAND_GENRE_LIMIT_EXCEEDED = "No more than 3 genres are allowed." diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index 697973e34..9d711b349 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -24,6 +24,8 @@ module JamRuby end def has_redeemable_jamtrack + raise "not a cookied anonymous user" if @cookies.nil? + APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack] end @@ -46,5 +48,9 @@ module JamRuby def signup_hint SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first end + + def reload + + end end end diff --git a/ruby/lib/jam_ruby/models/gift_card.rb b/ruby/lib/jam_ruby/models/gift_card.rb index 2c8e5d0d3..8a8469a27 100644 --- a/ruby/lib/jam_ruby/models/gift_card.rb +++ b/ruby/lib/jam_ruby/models/gift_card.rb @@ -3,34 +3,33 @@ module JamRuby @@log = Logging.logger[GiftCard] - FIVE_JAM_TRACKS = 'five_jam_tracks' - TEN_JAM_TRACKS = 'ten_jam_tracks' + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_TRACKS_20 = 'jam_tracks_20' CARD_TYPES = [ - FIVE_JAM_TRACKS, - TEN_JAM_TRACKS + JAM_TRACKS_10, + JAM_TRACKS_20 ] belongs_to :user, class_name: "JamRuby::User" validates :card_type, presence: true, inclusion: {in: CARD_TYPES} - validates :redeemed, inclusion: {in: [true, false]} validates :code, presence: true, uniqueness: true - def redeem(user) + after_save :check_gifted - transaction do - update({redeemed: true, user: user}) - - if card_type == FIVE_JAM_TRACK - user.red - elsif card_type == TEN_JAM_TRACK + def check_gifted + if user && user_id_changed? + if card_type == JAM_TRACKS_10 + user.gifted_jamtracks += 10 + elsif card_type == JAM_TRACKS_20 + user.gifted_jamtracks += 20 else - raise 'unknown type' + raise "unknown card type #{card_type}" end + user.save! end end - end end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 0103b3a21..4db356055 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -129,7 +129,29 @@ module JamRuby end def self.is_only_freebie(shopping_carts_jam_tracks) - shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free] + free = true + shopping_carts_jam_tracks.each do |cart| + free = cart.product_info[:free] + + if !free + break + end + end + free + end + + # we don't allow mixed shopping carts :/ + def self.is_mixed(shopping_carts) + free = false + non_free = false + shopping_carts.each do |cart| + if cart.product_info[:free] + free = true + else + non_free = true + end + end + free && non_free end # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index fe9ff5b74..41a4fdc14 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -12,6 +12,8 @@ module JamRuby attr_accessible :quantity, :cart_type, :product_info + attr_accessor :skip_mix_check + validates_uniqueness_of :cart_id, scope: [:cart_type, :user_id, :anonymous_user_id] belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id" @@ -20,6 +22,7 @@ module JamRuby validates :cart_type, presence: true validates :cart_class_name, presence: true validates :marked_for_redeem, numericality: {only_integer: true} + validate :not_mixed default_scope order('created_at DESC') @@ -38,6 +41,31 @@ module JamRuby (quantity - marked_for_redeem) * product.price end + def not_mixed + + return if @skip_mix_check + existing_carts = [] + this_user = any_user() + + if this_user + existing_carts = this_user.shopping_carts + end + + existing_carts = existing_carts.to_a + existing_carts << self + + if Sale.is_mixed(existing_carts) + if free? + errors.add(:base, "You can not add a free JamTrack to a cart with non-free items. Please clear out your cart.") + return false + else + errors.add(:base, "You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart.") + return false + end + end + + false + end def cart_product self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank? @@ -51,6 +79,16 @@ module JamRuby marked_for_redeem == quantity end + def any_user + if user + user + elsif anonymous_user_id + AnonymousUser.new(anonymous_user_id, nil) + else + nil + end + end + def self.create user, product, quantity = 1, mark_redeem = false cart = ShoppingCart.new if user.is_a?(User) @@ -81,6 +119,7 @@ module JamRuby if free? + puts "GOT A FREEBIE!" # create the credit, then the pseudo charge [ { @@ -197,6 +236,22 @@ module JamRuby end end + # if the number of items in the shopping cart is less than gifted_jamtracks on the user, then fix them all up + def self.apply_gifted_jamtracks(user) + jam_track_carts = user.shopping_carts.where(cart_type:JamTrack::PRODUCT_TYPE) + + if jam_track_carts.count > user.gifted_jamtracks + # can't do anything automatically + return + end + + jam_track_carts.each do |cart| + cart.skip_mix_check = true + cart.marked_for_redeem = 1 + cart.save! + end + + end def port(user, anonymous_user) ShoppingCart.transaction do diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 97587846e..1ba3a8b1b 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -40,7 +40,7 @@ module JamRuby attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection # updating_password corresponds to a lost_password - attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json + attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' @@ -148,6 +148,9 @@ module JamRuby # events has_many :event_sessions, :class_name => "JamRuby::EventSession" + # gift cards + has_many :gift_cards, :class_name=> "JamRuby::GiftCard" + # affiliate_partner has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count @@ -215,6 +218,7 @@ module JamRuby validate :email_case_insensitive_uniqueness validate :update_email_case_insensitive_uniqueness, :if => :updating_email validate :validate_mods + validate :presence_gift_card, :if => :expecting_gift_card scope :musicians, where(:musician => true) scope :fans, where(:musician => false) @@ -268,6 +272,12 @@ module JamRuby end end + def presence_gift_card + if self.gift_cards.length == 0 + errors.add(:gift_card, ValidationMessages::NOT_FOUND) + end + end + def validate_current_password # checks if the user put in their current password (used when changing your email, for instance) errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password) @@ -1038,6 +1048,7 @@ module JamRuby reuse_card = options[:reuse_card] signup_hint = options[:signup_hint] affiliate_partner = options[:affiliate_partner] + gift_card = options[:gift_card] user = User.new @@ -1049,12 +1060,8 @@ module JamRuby user.terms_of_service = terms_of_service user.musician = musician user.reuse_card unless reuse_card.nil? - - if APP_CONFIG.one_free_jamtrack_per_user - user.free_jamtracks = 1 - else - user.free_jamtracks = 0 - end + user.gifted_jamtracks = 0 + user.has_redeemable_jamtrack = true # FIXME: Setting random password for social network logins. This @@ -1160,6 +1167,13 @@ module JamRuby end end + # if a gift card value was passed in, then try to find that gift card and apply it to user + if gift_card + user.expecting_gift_card = true + found_gift_card = GiftCard.where(code:gift_card).where(user_id:nil).first + user.gift_cards << found_gift_card if found_gift_card + end + user.save # if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it @@ -1201,6 +1215,7 @@ module JamRuby end end end + user.reload if user.id# gift card adding gifted_jamtracks doesn't reflect here until reload user end # def signup diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index bb832b2e4..1a9097e68 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -860,4 +860,9 @@ FactoryGirl.define do legalese Faker::Lorem.paragraphs(6).join("\n\n") end + factory :gift_card, class: 'JamRuby::GiftCard' do + sequence(:code) {n.to_s} + card_type = JamRuby::GiftCard::JAM_TRACKS_10 + end + end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 685d8dfbd..6119a5973 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -5,6 +5,23 @@ describe Sale do let(:user) {FactoryGirl.create(:user)} let(:user2) {FactoryGirl.create(:user)} let(:jam_track) {FactoryGirl.create(:jam_track)} + let(:jam_track2) {FactoryGirl.create(:jam_track)} + let(:jam_track3) {FactoryGirl.create(:jam_track)} + + def assert_free_line_item(sale_line_item, jamtrack) + sale_line_item.recurly_tax_in_cents.should be_nil + sale_line_item.recurly_total_in_cents.should be_nil + sale_line_item.recurly_currency.should be_nil + sale_line_item.recurly_discount_in_cents.should be_nil + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(1) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + end describe "index" do it "empty" do @@ -47,6 +64,9 @@ describe Sale do let(:user) {FactoryGirl.create(:user)} let(:jamtrack) { FactoryGirl.create(:jam_track) } + let(:jamtrack2) { FactoryGirl.create(:jam_track) } + let(:jamtrack3) { FactoryGirl.create(:jam_track) } + let(:jamtrack4) { FactoryGirl.create(:jam_track) } let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i } let(:client) { RecurlyClient.new } let(:billing_info) { @@ -87,6 +107,7 @@ describe Sale do sales.should eq(user.sales) sale = sales[0] + sale.recurly_invoice_id.should be_nil sale.recurly_subtotal_in_cents.should eq(0) @@ -132,6 +153,92 @@ describe Sale do user.has_redeemable_jamtrack.should be_false end + it "for two jam tracks (1 freebie, 1 gifted), then 1 gifted/1 pay" do + user.gifted_jamtracks = 2 + user.save! + + shopping_cart1 = ShoppingCart.create user, jamtrack, 1, true + shopping_cart2 = ShoppingCart.create user, jamtrack2, 1, true + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart1, shopping_cart2]) + + user.reload + user.sales.length.should eq(1) + sale = sales[0] + sale.reload + + sale.recurly_invoice_id.should be_nil + + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 2 + + assert_free_line_item(sale.sale_line_items[0], jamtrack) + assert_free_line_item(sale.sale_line_items[1], jamtrack2) + + # verify jam_track_rights data + right1 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack.id).first + right2 = JamTrackRight.where(user_id: user.id).where(jam_track_id: jamtrack2.id).first + user.jam_track_rights.should have(2).items + + right1.redeemed.should be_true + right2.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + user.gifted_jamtracks.should eq(1) + + + + # OK! Now make a second purchase; this time, buy one free, one not free + shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true + shopping_cart4 = ShoppingCart.create user, jamtrack4, 1, false + + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart3, shopping_cart4]) + + user.reload + user.sales.length.should eq(2) + sale = sales[0] + sale.reload + + sale.recurly_invoice_id.should_not be_nil + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 2 + + assert_free_line_item(sale.sale_line_items[0], jamtrack3) + + paid_right = JamTrackRight.where(user_id:user.id).where(jam_track_id: jamtrack4.id).first + + sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack4.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(0) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack4.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(paid_right.recurly_adjustment_uuid) + + user.has_redeemable_jamtrack.should be_false + user.gifted_jamtracks.should eq(0) + + end + it "for a free jam track with an affiliate association" do partner = FactoryGirl.create(:affiliate_partner) user.affiliate_referral = partner diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index 85319a7e0..58a528c7b 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -18,6 +18,9 @@ describe ShoppingCart do it "can reference a shopping cart" do shopping_cart = ShoppingCart.create user, jam_track, 1 + shopping_cart.errors.any?.should be_false + shopping_cart.valid?.should be_true + user.reload ShoppingCart.count.should == 1 user.shopping_carts.count.should == 1 user.shopping_carts[0].product_info[:name].should == jam_track.name @@ -86,7 +89,7 @@ describe ShoppingCart do user.save! end - it "user can add and remove jamtracks without issue" do + it "user can add and remove jamtracks without issue, until 'mixed' free/non-free is hit" do cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart1.should_not be_nil cart1.errors.any?.should be_false @@ -138,48 +141,51 @@ describe ShoppingCart do cart6.marked_for_redeem.should eq(1) cart7 = ShoppingCart.add_jam_track_to_cart(user, jam_track7) - cart7.errors.any?.should be_false + cart7.errors.any?.should be_true user.reload - user.shopping_carts.length.should eq(7) + user.shopping_carts.length.should eq(6) cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) cart3.marked_for_redeem.should eq(1) cart4.marked_for_redeem.should eq(1) cart5.marked_for_redeem.should eq(1) cart6.marked_for_redeem.should eq(1) - # finally, a non-free one - cart7.marked_for_redeem.should eq(0) + end + end - - # Now remove one of the free jamtracks, and we should see the one-non free jamtrack (jam_track6) become free - ShoppingCart.remove_jam_track_from_cart(user, cart1) - - cart1.destroyed?.should be true - - user.reload - user.shopping_carts.length.should eq(6) - cart1.destroyed?.should be true - cart2.reload - cart3.reload - cart4.reload - cart5.reload - cart6.reload - cart7.reload - cart2.marked_for_redeem.should eq(1) - cart3.marked_for_redeem.should eq(1) - cart4.marked_for_redeem.should eq(1) - cart5.marked_for_redeem.should eq(1) - cart6.marked_for_redeem.should eq(1) - # cart7 should have changed to free automatically - cart7.marked_for_redeem.should eq(1) + describe "mixed" do + it "non-free then free" do + # you shouldn't be able to add a free after a non-free + user.has_redeemable_jamtrack = false + user.save! cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart1.should_not be_nil cart1.errors.any?.should be_false - cart1.marked_for_redeem.should eq(0) + user.has_redeemable_jamtrack = true + user.save! + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a free JamTrack to a cart with non-free items. Please clear out your cart."]) + user.shopping_carts.length.should eq(1) + end + it "free then non-free" do + + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + cart1.errors.any?.should be_false + + user.reload + + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart2.errors.any?.should be_true + cart2.errors[:base].should eq(["You can not add a non-free JamTrack to a cart containing free items. Please clear out your cart."]) + + user.shopping_carts.length.should eq(1) end end end diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 674724c48..b2d7a604a 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1800,7 +1800,7 @@ }); deferred.done(function(response) { - window.UserActions.modify({response}) + window.UserActions.modify(response) }) return deferred } @@ -1824,7 +1824,7 @@ }) deferred.done(function(response) { - window.UserActions.modify({response}) + window.UserActions.modify(response) }) return deferred } @@ -1997,6 +1997,17 @@ }); } + function redeemGiftCard(data) { + var id = getId(data); + return $.ajax({ + type: "POST", + url: '/api/users/' + id + '/gift_cards', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + function portOverCarts() { return $.ajax({ type: "POST", @@ -2201,6 +2212,7 @@ this.playJamTrack = playJamTrack; this.createSignupHint = createSignupHint; this.createAlert = createAlert; + this.redeemGiftCard = redeemGiftCard; this.signup = signup; this.portOverCarts = portOverCarts; return this; diff --git a/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee index 669861ad1..8bf7b2025 100644 --- a/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee @@ -1,32 +1,159 @@ context = window rest = context.JK.Rest() +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; @RedeemGiftCardPage = React.createClass({ render: () -> + + + if this.state.formErrors? + for key, value of this.state.formErrors + break + + errorText = context.JK.getFullFirstError(key, @state.formErrors, {email: 'Email', password: 'Password', gift_card: 'Gift Card Code', 'terms_of_service' : 'The terms of service'}) + + buttonClassnames = classNames({'redeem-giftcard': true, 'button-orange': true, disabled: @state.processing || @state.done }) + + if @state.done + button = + `
+
You have {this.state.gifted_jamtracks} free JamTracks on your account!
+
You can now browse our collection and redeem them.
+
` + else + button = `` + + action = ` + {button} + ` + + if context.JK.currentUserId? form = - `
+ ` - + {action}
` instruments = `

Enter the code from the back of your gift card to associate it with your account.

` else form = - `
+ ` - +
+ +
+ {action} ` instruments = `

Enter the code from the back of your gift card to associate it with your new JamKazam account.

` - `
+ classes = classNames({'redeem-container': true, 'not-logged-in': !context.JK.currentUserId?, 'logged-in': context.JK.currentUserId? }) + `

Redeem Your Gift Card

{instruments} {form} +
+ {errorText} +
` + + getInitialState: () -> + {formErrors: null, processing:false, gifted_jamtracks: null} + + privacyPolicy: (e) -> + e.preventDefault() + + context.JK.popExternalLink('/corp/privacy') + + termsClicked: (e) -> + e.preventDefault() + + context.JK.popExternalLink('/corp/terms') + + componentDidMount:() -> + $root = $(this.getDOMNode()) + $checkbox = $root.find('.terms-checkbox') + console.log("$checkbox", $checkbox) + context.JK.checkbox($checkbox) + + submit: (e) -> + @action(e) + action: (e) -> + + if @state.done || @state.processing + e.preventDefault() + return + + if context.JK.currentUserId? + @redeem(e) + else + @signup(e) + + redeem: (e) -> + e.preventDefault() + return if @state.done || @state.processing + + $root = $(@getDOMNode()) + $code = $root.find('input[name="code"]') + code = $code.val() + + rest.redeemGiftCard({gift_card: code}) + .done((response) => + + @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks}) + + ).fail((jqXHR) => + @setState({processing:false}) + + if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + if response.errors + @setState({formErrors: response.errors}) + else + context.JK.app.notify({title: 'Unknown Error', text: jqXHR.responseText}) + else + context.JK.app.notifyServerError(jqXHR, "Unable to Redeem Giftcard") + ) + + signup: (e) -> + e.preventDefault() + + return if @state.done || @state.processing + + $root = $(@getDOMNode()) + $email = $root.find('input[name="email"]') + $code = $root.find('input[name="code"]') + $password = $root.find('input[name="password"]') + terms = $root.find('input[name="terms"]').is(':checked') + + @setState({processing:true}) + email = $email.val() + password = $password.val() + code = $code.val() + if !code + # must pass up non-null value to indicate user is trying to redeem giftcard while creating account + code = '' + + rest.signup({email: email, password: password, gift_card: code, terms: terms}) + .done((response) => + + @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks}) + + ).fail((jqXHR) => + @setState({processing:false}) + + if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + if response.errors + @setState({formErrors: response.errors}) + else + context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText}) + else + context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up") + ) }) \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 6af987666..292e6c631 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -702,4 +702,23 @@ $ReactSelectVerticalPadding: 3px; .Select-search-prompt { padding:3px 0 !important; -} \ No newline at end of file +} + + +.session-track-list-enter { + opacity: 0.01; + transition: opacity .5s ease-in; + + &.session-track-list-enter-active { + opacity: 1; + } +} + +.session-track-list-leave { + opacity:1; + transition: opacity .5s ease-in; + + &.session-track-list-leave-active { + opacity: 0.01; + } +} diff --git a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss index e7e510b2e..bea31605f 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss @@ -386,22 +386,3 @@ $session-screen-divider: 1190px; } } } - - -.session-track-list-enter { - opacity: 0.01; - transition: opacity .5s ease-in; - - &.session-track-list-enter-active { - opacity: 1; - } -} - -.session-track-list-leave { - opacity:1; - transition: opacity .5s ease-in; - - &.session-track-list-leave-active { - opacity: 0.01; - } -} diff --git a/web/app/assets/stylesheets/landings/redeem_giftcard.css.scss b/web/app/assets/stylesheets/landings/redeem_giftcard.css.scss index 0f6078ff2..c1cd5af24 100644 --- a/web/app/assets/stylesheets/landings/redeem_giftcard.css.scss +++ b/web/app/assets/stylesheets/landings/redeem_giftcard.css.scss @@ -21,11 +21,15 @@ body.web.redeem_giftcard { width:400px; padding-top:20px; - &.not-logged-in { + &.logged-in { button { margin-top:10px !important; } } + + &.not-logged-in { + + } } .redeem-content { @@ -45,8 +49,42 @@ body.web.redeem_giftcard { padding: 7px 3px !important; line-height:inherit !important; margin-left:2px !important; - margin-top:30px; + margin-top:15px; } + .icheckbox_minimal { + float: left; + top: -2px; + margin-left: 0; + margin-right:10px; + } + + .errors { + font-size:14px; + height:20px; + margin:0; + visibility: hidden; + color: red; + font-weight: bold; + + &.active { + visibility: visible; + } + } + + form { + margin-bottom:20px; + } + .terms-help { + float:left; + margin-top:-5px; + font-size:12px; + width:178px; + } + + .done-action { + margin-top: 20px; + line-height: 125%; + } } \ No newline at end of file diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index a76113739..e051e17fc 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -126,8 +126,15 @@ class ApiRecurlyController < ApiController error=nil response = {jam_tracks: []} + if Sale.is_mixed(current_user.shopping_carts) + msg = "has free and non-free items. Try removing non-free items." + render json: {message: "Cart " + msg, errors: {cart: [msg]}}, :status => 404 + return + end + sales = Sale.place_order(current_user, current_user.shopping_carts) + sales.each do |sale| if sale.is_jam_track_sale? sale.sale_line_items.each do |line_item| diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index ad8ec6583..05eed5887 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -14,8 +14,8 @@ ApiUsersController < ApiController :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, :share_session, :share_recording, - :affiliate_report, :audio_latency, :broadcast_notification] - before_filter :ip_blacklist, :only => [:create] + :affiliate_report, :audio_latency, :broadcast_notification, :redeem_giftcard] + before_filter :ip_blacklist, :only => [:create, :redeem_giftcard] respond_to :json, :except => :calendar respond_to :ics, :only => :calendar @@ -81,6 +81,7 @@ ApiUsersController < ApiController terms_of_service: params[:terms], location: {:country => nil, :state => nil, :city => nil}, signup_hint: signup_hint, + gift_card: params[:gift_card], affiliate_referral_id: cookies[:affiliate_visitor] } @@ -919,6 +920,45 @@ ApiUsersController < ApiController .find(params[:id]) end + def redeem_giftcard + @gift_card = GiftCard.find_by_code(params[:gift_card]) + + if @gift_card.nil? + render json: {errors:{gift_card: ['does not exist']}}, status: 422 + return + end + + if current_user.gift_cards.count >= 5 + render json: {errors:{gift_card: ['has too many on account']}}, status: 422 + return + end + + if @gift_card.user + if @gift_card.user == current_user + render json: {errors:{gift_card: ['already redeemed by you']}}, status: 422 + return + else + render json: {errors:{gift_card: ['already redeemed by another']}}, status: 422 + return + end + end + + @gift_card.user = current_user + @gift_card.save + + if @gift_card.errors.any? + respond_with_model(@gift_card) + return + else + + # apply gift card items to everything in shopping cart + current_user.reload + ShoppingCart.apply_gifted_jamtracks(current_user) + render json: {gifted_jamtracks:current_user.gifted_jamtracks}, status: 200 + end + end + + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/app/views/api_shopping_carts/remove_cart.rabl b/web/app/views/api_shopping_carts/remove_cart.rabl index 7ae0bb896..35e9a2b48 100644 --- a/web/app/views/api_shopping_carts/remove_cart.rabl +++ b/web/app/views/api_shopping_carts/remove_cart.rabl @@ -1,3 +1,3 @@ node :show_free_jamtrack do - any_user.user.show_free_jamtrack? + any_user.show_free_jamtrack? end \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 4ff2e057a..0dd83c6ae 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -48,7 +48,6 @@ <%= render "checkout_complete" %> <%= render "redeem_signup" %> <%= render "redeem_complete" %> -<%= render "order" %> <%= render "feed" %> <%= render "bands" %> <%= render "musicians" %> diff --git a/web/config/routes.rb b/web/config/routes.rb index bfdcc0ad9..4c66edfc4 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -454,6 +454,9 @@ SampleApp::Application.routes.draw do match '/users/:id/syncs/:user_sync_id' => 'api_user_syncs#show', :via => :get match '/users/:id/syncs/deletables' => 'api_user_syncs#deletables', :via => :post + # giftcards + match '/users/:id/gift_cards' => 'api_users#redeem_giftcard', :via => :post + # bands diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb index 8f231a57b..5fd12f487 100644 --- a/web/lib/user_manager.rb +++ b/web/lib/user_manager.rb @@ -29,6 +29,7 @@ class UserManager < BaseManager any_user = options[:any_user] signup_hint = options[:signup_hint] affiliate_partner = options[:affiliate_partner] + gift_card = options[:gift_card] recaptcha_failed = false unless options[:skip_recaptcha] # allow callers to opt-of recaptcha @@ -72,7 +73,8 @@ class UserManager < BaseManager affiliate_referral_id: affiliate_referral_id, any_user: any_user, signup_hint: signup_hint, - affiliate_partner: affiliate_partner) + affiliate_partner: affiliate_partner, + gift_card: gift_card) user end diff --git a/web/spec/controllers/api_shopping_carts_controller_spec.rb b/web/spec/controllers/api_shopping_carts_controller_spec.rb index c6f196d0d..b5efb7fa8 100644 --- a/web/spec/controllers/api_shopping_carts_controller_spec.rb +++ b/web/spec/controllers/api_shopping_carts_controller_spec.rb @@ -25,11 +25,14 @@ describe ApiShoppingCartsController do it "add_jamtrack" do post :add_jamtrack, {:format => 'json', id: jam_track.id} - response.status.should == 201 + response.status.should == 200 end it "index" do cart = ShoppingCart.create(user, jam_track) + cart.errors.any?.should be_false + user.reload + user.shopping_carts.count.should eq(1) get :index, {:format => 'json'} response.status.should == 200 @@ -41,7 +44,7 @@ describe ApiShoppingCartsController do it "remove_cart" do cart = ShoppingCart.create(user, jam_track) delete :remove_cart, {:format => 'json', id: cart.id} - response.status.should == 204 + response.status.should == 200 ShoppingCart.find_by_id(cart.id).should be_nil end @@ -64,7 +67,9 @@ describe ApiShoppingCartsController do it "add_jamtrack" do post :add_jamtrack, {:format => 'json', id: jam_track.id} - response.status.should == 201 + response.status.should == 200 + user.reload + user.shopping_carts.count.should eql(1) end it "index" do @@ -80,7 +85,7 @@ describe ApiShoppingCartsController do it "remove_cart" do cart = ShoppingCart.create(user, jam_track) delete :remove_cart, {:format => 'json', id: cart.id} - response.status.should == 204 + response.status.should == 200 ShoppingCart.find_by_id(cart.id).should be_nil end diff --git a/web/spec/controllers/api_users_controller_spec.rb b/web/spec/controllers/api_users_controller_spec.rb index 065602e77..6c7d83fdb 100644 --- a/web/spec/controllers/api_users_controller_spec.rb +++ b/web/spec/controllers/api_users_controller_spec.rb @@ -5,12 +5,134 @@ describe ApiUsersController do let (:user) { FactoryGirl.create(:user) } let (:conn) { FactoryGirl.create(:connection, user: user, last_jam_audio_latency: 5) } + let (:jam_track) { FactoryGirl.create(:jam_track)} before(:each) do controller.current_user = user end + describe "redeem_giftcard" do + let!(:gift_card) {FactoryGirl.create(:gift_card)} + + it "can succeed" do + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.should be_success + + user.reload + gift_card.reload + + user.gift_cards.should eq([gift_card]) + user.gifted_jamtracks.should eq(10) + gift_card.user.should eq(user) + end + + it "indicates if you've redeemed it" do + gift_card.user = user + gift_card.save! + + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.status.should eq(422) + error_data = JSON.parse(response.body) + error_data['errors']['gift_card'].should eq(["already redeemed by you"]) + + user.reload + gift_card.reload + + user.gift_cards.should eq([gift_card]) + user.gifted_jamtracks.should eq(10) + gift_card.user.should eq(user) + end + + it "indicates if someone else has redeemed it" do + user2 = FactoryGirl.create(:user) + gift_card.user = user2 + gift_card.save! + + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.status.should eq(422) + error_data = JSON.parse(response.body) + error_data['errors']['gift_card'].should eq(["already redeemed by another"]) + + user.reload + gift_card.reload + + user.gift_cards.should eq([]) + user.gifted_jamtracks.should eq(0) + gift_card.user.should eq(user2) + end + + it "marks free shopping cart item as free" do + # sort of a 'do nothing' really + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.marked_for_redeem.should eq(1) + + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.should be_success + + user.reload + gift_card.reload + + user.gift_cards.should eq([gift_card]) + user.gifted_jamtracks.should eq(10) + gift_card.user.should eq(user) + cart1.reload + cart1.marked_for_redeem.should eq(1) + end + + it "marks non-free shopping cart item as free" do + # sort of a 'do nothing' really + user.has_redeemable_jamtrack = false + user.save! + + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.marked_for_redeem.should eq(0) + + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.should be_success + + user.reload + gift_card.reload + + user.gift_cards.should eq([gift_card]) + user.gifted_jamtracks.should eq(10) + gift_card.user.should eq(user) + cart1.reload + cart1.marked_for_redeem.should eq(1) + end + + it "leaves shopping cart alone if too many items in it for size of new gift card" do + # sort of a 'do nothing' really + user.has_redeemable_jamtrack = false + user.save! + + 11.times do |i| + jamtrack = FactoryGirl.create(:jam_track) + cart1 = ShoppingCart.add_jam_track_to_cart(user, jamtrack) + cart1.marked_for_redeem.should eq(0) + end + + post :redeem_giftcard, id:user.id, gift_card: gift_card.code, format:'json' + + response.should be_success + + user.reload + gift_card.reload + + user.gift_cards.should eq([gift_card]) + user.gifted_jamtracks.should eq(10) + gift_card.user.should eq(user) + user.shopping_carts.each do |cart| + cart.marked_for_redeem.should eq(0) + end + end + end + describe "create" do it "successful" do email = 'user_create1@jamkazam.com' diff --git a/web/spec/factories.rb b/web/spec/factories.rb index ed295012d..4ff2d50ea 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -839,4 +839,9 @@ FactoryGirl.define do factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do legalese Faker::Lorem.paragraphs(6).join("\n\n") end + + factory :gift_card, class: 'JamRuby::GiftCard' do + sequence(:code) {n.to_s} + card_type GiftCard::JAM_TRACKS_10 + end end diff --git a/web/spec/features/redeem_giftcard_spec.rb b/web/spec/features/redeem_giftcard_spec.rb new file mode 100644 index 000000000..fd0c7f436 --- /dev/null +++ b/web/spec/features/redeem_giftcard_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +# tests what happens when the websocket connection goes away +describe "Redeem Gift Card", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + + before(:all) do + User.delete_all + end + + describe "not logged in" do + it "suceeeds" do + visit '/redeem_giftcard' + + find('h2', text:'Redeem Your Gift Card') + fill_in "email", with: "gifter1@jamkazam.com" + fill_in "password", with: "jam123" + find('.redeem-container ins').trigger(:click) + + find('button.redeem-giftcard').trigger(:click) + + find('.done-action a.go-browse').trigger(:click) + + find('.no-free-jamtrack') + end + + it "validates correctly" do + + end + end +end diff --git a/web/spec/managers/user_manager_spec.rb b/web/spec/managers/user_manager_spec.rb index f6d464226..974fbf031 100644 --- a/web/spec/managers/user_manager_spec.rb +++ b/web/spec/managers/user_manager_spec.rb @@ -707,4 +707,77 @@ describe UserManager do user.errors.any?.should be_false end # it "passes when facebook signup" end # describe "with nocaptcha" + + describe "gift_card" do + + let(:gift_card) {FactoryGirl.create(:gift_card)} + + it "can succeed when specified" do + user = @user_manager.signup(remote_ip: "1.2.3.4", + first_name: "bob", + last_name: "smith", + email: "giftcard1@jamkazam.com", + password: "foobar", + password_confirmation: "foobar", + terms_of_service: true, + instruments: @instruments, + musician: true, + location: @loca, + signup_confirm_url: "http://localhost:3000/confirm", + gift_card: gift_card.code) + user.errors.any?.should be_false + gift_card.reload + gift_card.user.should eq(user) + user = User.find(user.id) + user.has_redeemable_jamtrack.should be_true + user.gifted_jamtracks.should eq(10) + user.gift_cards[0].should eq(gift_card) + end + + it "will fail if invalid gift card code" do + user = @user_manager.signup(remote_ip: "1.2.3.4", + first_name: "bob", + last_name: "smith", + email: "giftcard2@jamkazam.com", + password: "foobar", + password_confirmation: "foobar", + terms_of_service: true, + instruments: @instruments, + musician: true, + location: @loca, + signup_confirm_url: "http://localhost:3000/confirm", + gift_card: '') + user.errors.any?.should be_true + user.errors["gift_card"].should eq(["not found"]) + user.gifted_jamtracks.should eq(0) + gift_card.reload + gift_card.user.should be_nil + + user.gift_cards.length.should eq(0) + end + + it "will fail if used gift card" do + gift_card.user = FactoryGirl.create(:user) + + user = @user_manager.signup(remote_ip: "1.2.3.4", + first_name: "bob", + last_name: "smith", + email: "giftcard2@jamkazam.com", + password: "foobar", + password_confirmation: "foobar", + terms_of_service: true, + instruments: @instruments, + musician: true, + location: @loca, + signup_confirm_url: "http://localhost:3000/confirm", + gift_card: '') + user.errors.any?.should be_true + user.errors["gift_card"].should eq(["not found"]) + user.gifted_jamtracks.should eq(0) + gift_card.reload + gift_card.user.should be_nil + + user.gift_cards.length.should eq(0) + end + end end # test