diff --git a/admin/app/admin/gift_card_upload.rb b/admin/app/admin/gift_card_upload.rb new file mode 100644 index 000000000..19f1e2df9 --- /dev/null +++ b/admin/app/admin/gift_card_upload.rb @@ -0,0 +1,41 @@ +ActiveAdmin.register_page "Giftcarduploads" do + + menu :label => 'Gift Cards Upload', :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_giftcarduploads_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/app/admin/gift_cards.rb b/admin/app/admin/gift_cards.rb new file mode 100644 index 000000000..8e1d4e80d --- /dev/null +++ b/admin/app/admin/gift_cards.rb @@ -0,0 +1,24 @@ +ActiveAdmin.register JamRuby::GiftCard, :as => 'GiftCards' do + + menu :label => 'Gift Cards', :parent => 'JamTracks' + + config.batch_actions = false + config.filters = true + config.per_page = 50 + + scope("Redeemed Most Recently", default: true) { |scope| scope.where('user_id IS NOT NULL').order('updated_at DESC') } + scope("Available") { |scope| scope.where('user_id is NULL') } + + filter :card_type + filter :origin + filter :code + + index do + column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end + column 'Code' do |oo| oo.code end + column 'Card Type' do |oo| oo.card_type end + column 'Origin' do |oo| oo.origin end + column 'Created' do |oo| oo.created_at 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/manifest b/db/manifest index 4da7a1b28..bc0c3e7c2 100755 --- a/db/manifest +++ b/db/manifest @@ -310,3 +310,4 @@ web_playable_jamtracks.sql affiliate_partner_rate.sql track_downloads.sql jam_track_lang_idx.sql +giftcard.sql diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql new file mode 100644 index 000000000..eae33c13e --- /dev/null +++ b/db/up/giftcard.sql @@ -0,0 +1,13 @@ +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, + 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_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 24bc2b104..9d711b349 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -24,11 +24,33 @@ 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 + def gifted_jamtracks + 0 + end + + def free_jamtracks + if has_redeemable_jamtrack + 1 + else + 0 + end + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + 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 new file mode 100644 index 000000000..8a8469a27 --- /dev/null +++ b/ruby/lib/jam_ruby/models/gift_card.rb @@ -0,0 +1,35 @@ +module JamRuby + class GiftCard < ActiveRecord::Base + + @@log = Logging.logger[GiftCard] + + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_TRACKS_20 = 'jam_tracks_20' + CARD_TYPES = + [ + JAM_TRACKS_10, + JAM_TRACKS_20 + ] + + + belongs_to :user, class_name: "JamRuby::User" + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + validates :code, presence: true, uniqueness: true + + after_save :check_gifted + + 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 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 70279f8eb..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) @@ -311,10 +333,17 @@ module JamRuby jam_track_right.redeemed = shopping_cart.free? end - # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks + # also if the purchase was a free one, then: + # first, mark the free has_redeemable_jamtrack field if that's still true + # and if still they have more free things, then redeem the giftable_jamtracks if shopping_cart.free? - User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) - current_user.has_redeemable_jamtrack = false # make sure model reflects the truth + if user.has_redeemable_jamtrack + User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) + current_user.has_redeemable_jamtrack = false + else + User.where(id: current_user.id).update_all(gifted_jamtracks: current_user.gifted_jamtracks - 1) + current_user.gifted_jamtracks = current_user.gifted_jamtracks - 1 + end end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index 568d94048..c113c5a99 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 [ { @@ -134,28 +173,32 @@ module JamRuby # if no shpping carts have been marked, then mark it redeemable # should be wrapped in a TRANSACTION def self.user_has_redeemable_jam_track?(any_user) - mark_redeem = false - if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack - mark_redeem = true # start out assuming we can redeem... + + if any_user.has_redeemable_jamtrack || any_user.gifted_jamtracks > 0 + + free_in_cart = 0 any_user.shopping_carts.each do |shopping_cart| # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true - if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0 - mark_redeem = false - break + if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE + free_in_cart += shopping_cart.marked_for_redeem end end + + any_user.free_jamtracks > free_in_cart + else + false end - mark_redeem end # adds a jam_track to cart, checking for promotions - def self.add_jam_track_to_cart(any_user, jam_track) + def self.add_jam_track_to_cart(any_user, jam_track, clear:false) cart = nil ShoppingCart.transaction do - if any_user.has_redeemable_jamtrack - # if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart + if clear + # if you are an anonymous user, we make sure there is nothing else in your shopping cart ... keep it clean for the 'new user rummaging around for a freebie scenario' any_user.destroy_all_shopping_carts + any_user.reload end mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) @@ -168,19 +211,48 @@ module JamRuby def self.remove_jam_track_from_cart(any_user, cart) ShoppingCart.transaction do cart.destroy - # check if we should move the redemption + + # so that user.shopping_carts reflects truth + any_user.reload + + # check if we should move the redemption around automatically mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) carts = any_user.shopping_carts - # if we find any carts on the account, mark one redeemable + # if we find any carts on the account that are not redeemed, mark first one redeemable if mark_redeem && carts.length > 0 - carts[0].redeem(mark_redeem) - carts[0].save + carts.each do |cart| + if cart.marked_for_redeem == 0 + if cart.quantity > 1 + raise 'unknown situation for redeemption juggling' + end + cart.redeem(mark_redeem) + cart.save + break + end + end end 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 + # just whack everything in their shopping cart + user.destroy_all_shopping_carts + 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 a9c46801a..f802954ea 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 @@ -194,6 +197,7 @@ module JamRuby validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false } validates :reuse_card, :inclusion => {:in => [true, false]} validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} + validates :gifted_jamtracks, presence: true, :numericality => { :less_than_or_equal_to => 100 } validates :subscribe_email, :inclusion => {:in => [nil, true, false]} validates :musician, :inclusion => {:in => [true, false]} validates :show_whats_next, :inclusion => {:in => [nil, true, false]} @@ -214,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) @@ -233,6 +238,18 @@ module JamRuby end end + def has_any_free_jamtracks + has_redeemable_jamtrack || gifted_jamtracks > 0 + end + + def free_jamtracks + (has_redeemable_jamtrack ? 1 : 0) + gifted_jamtracks + end + + def show_free_jamtrack? + ShoppingCart.user_has_redeemable_jam_track?(self) + end + def failed_qualification(reason) self.last_failed_certified_gear_at = DateTime.now self.last_failed_certified_gear_reason = reason @@ -255,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) @@ -1025,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 @@ -1036,6 +1060,9 @@ module JamRuby user.terms_of_service = terms_of_service user.musician = musician user.reuse_card unless reuse_card.nil? + user.gifted_jamtracks = 0 + user.has_redeemable_jamtrack = true + # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. @@ -1140,8 +1167,22 @@ module JamRuby end end + found_gift_card = nil + + # 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 found_gift_card + user.reload + ShoppingCart.apply_gifted_jamtracks(user) + end + # if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it # only_freebie_in_cart = # signup_hint && @@ -1181,6 +1222,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 c8c91241e..1a9097e68 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -19,6 +19,8 @@ FactoryGirl.define do terms_of_service true last_jam_audio_latency 5 reuse_card true + has_redeemable_jamtrack true + gifted_jamtracks 0 #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -858,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 6be02e8d4..58a528c7b 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -3,8 +3,13 @@ require 'spec_helper' describe ShoppingCart do let(:user) { FactoryGirl.create(:user) } - let(:jam_track) {FactoryGirl.create(:jam_track) } - let(:jam_track2) {FactoryGirl.create(:jam_track) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:jam_track4) { FactoryGirl.create(:jam_track) } + let(:jam_track5) { FactoryGirl.create(:jam_track) } + let(:jam_track6) { FactoryGirl.create(:jam_track) } + let(:jam_track7) { FactoryGirl.create(:jam_track) } before(:each) do ShoppingCart.delete_all @@ -13,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 @@ -22,16 +30,16 @@ describe ShoppingCart do end - it "maintains only one fre JamTrack in ShoppingCart" do - cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + it "maintains only one free JamTrack in ShoppingCart" do + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart1.should_not be_nil cart1.errors.any?.should be_false user.reload - cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart2.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) - cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) cart3.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) @@ -53,19 +61,131 @@ describe ShoppingCart do it "removes redeemable item to shopping cart (maintains only one in cart)" do user.has_redeemable_jamtrack.should be_true + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) + cart1.should_not be_nil + cart1.errors.any?.should be_false + cart1.marked_for_redeem.should eq(1) + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart2.should_not be_nil + cart2.errors.any?.should be_false + cart2.marked_for_redeem.should eq(1) + + ShoppingCart.find_by_id(cart1.id).should be nil + + + ShoppingCart.remove_jam_track_from_cart(user, cart2) + + user.reload + user.shopping_carts.length.should eq(0) + ShoppingCart.find_by_id(cart2.id).should be nil + end + end + + describe "multiple free jamtracks" do + + before(:each) do + user.gifted_jamtracks = 5 + user.save! + end + + 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 + user.reload cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) - cart2.should_not be_nil - + cart2.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(2) cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) - ShoppingCart.remove_jam_track_from_cart(user, jam_track) - user.shopping_carts.length.should eq(0) - cart2.reload + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track3) + cart3.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(3) + cart1.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1) + cart3.marked_for_redeem.should eq(1) + + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track4) + cart4.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(4) + 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 = ShoppingCart.add_jam_track_to_cart(user, jam_track5) + cart5.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(5) + 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 = ShoppingCart.add_jam_track_to_cart(user, jam_track6) + cart6.errors.any?.should be_false + user.reload + 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) + + cart7 = ShoppingCart.add_jam_track_to_cart(user, jam_track7) + cart7.errors.any?.should be_true + user.reload + 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) + end + end + + 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 + + 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/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index 9b72f4fc5..956c11886 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -95,7 +95,7 @@ $reuseExistingCardChk.iCheck(userDetail.reuse_card && userDetail.has_recurly_account ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card) // show appropriate prompt text based on whether user has a free jamtrack - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $freeJamTrackPrompt.removeClass('hidden') } else { diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index c16e09f9b..c789ba71d 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -55,6 +55,16 @@ class CheckoutUtils return carts[0].product_info.free + hasOnlyFreeItemsInShoppingCart: (carts) => + if carts.length == 0 + return false + + for cart in carts + if !cart.product_info.free + return false + + return true + configureRecurly: () => unless @configuredRecurly context.recurly.configure(gon.global.recurly_public_api_key) diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 867a411f2..aa35e80e7 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -50,7 +50,7 @@ $browserJamTrackBtn.click(function() { app.layout.closeDialog('getting-started') - window.location = '/client#/jamtrack/search' + window.location = '/client#/jamtrack' return false; }) @@ -69,9 +69,9 @@ function beforeShow() { app.user().done(function(user) { - var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' + var jamtrackRule = user.has_redeemable_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack' $jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule) - if(user.free_jamtrack) { + if(user.has_redeemable_jamtrack) { $jamTracksLimitedTime.removeClass('hidden') } }) diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 2dcaabfc3..b2d7a604a 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1187,6 +1187,7 @@ }); deferred.done(function(user) { + context.JK.currentUserFreeJamTrack = user.show_free_jamtrack window.UserActions.loaded(user) }) @@ -1791,12 +1792,17 @@ } function addJamtrackToShoppingCart(options) { - return $.ajax({ + var deferred = $.ajax({ type: "POST", url: '/api/shopping_carts/add_jamtrack?' + $.param(options), dataType: "json", contentType: 'application/json' }); + + deferred.done(function(response) { + window.UserActions.modify(response) + }) + return deferred } function getShoppingCarts() { @@ -1810,12 +1816,17 @@ } function removeShoppingCart(options) { - return $.ajax({ + var deferred = $.ajax({ type: "DELETE", url: '/api/shopping_carts?' + $.param(options), dataType: "json", contentType: 'application/json' }) + + deferred.done(function(response) { + window.UserActions.modify(response) + }) + return deferred } function clearShoppingCart(options) { @@ -1986,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", @@ -2190,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/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee deleted file mode 100644 index bb0c2fa2b..000000000 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ /dev/null @@ -1,484 +0,0 @@ -$ = jQuery -context = window -context.JK ||= {} - -context.JK.JamTrackScreen=class JamTrackScreen - LIMIT = 10 - instrument_logo_map = context.JK.getInstrumentIconMap24() - - constructor: (@app) -> - @EVENTS = context.JK.EVENTS - @logger = context.JK.logger - @screen = null - @content = null - @scroller = null - @genre = null - @artist = null - @instrument = null - @availability = null - @nextPager = null - @noMoreJamtracks = null - @currentPage = 0 - @next = null - @currentQuery = this.defaultQuery() - @expanded = null - @shownHelperBubbles = false - - beforeShow:(data) => - this.setFilterFromURL() - - if context.JK.currentUserId? - @app.user().done((user) => - @user = user - this.refresh() - ).fail((arg) => - @logger.error("app.user.done failed: " + JSON.stringify(arg)) - - @logger.debug(arg.statusCode); - - throw 'fail should not occur if user is available' - ) - else - this.refresh() - unless @shownHelperBubbles - @shownHelperBubbles = true - @startHelperBubbles() - - afterShow:(data) => - context.JK.Tracking.jamtrackBrowseTrack(@app) - - beforeHide: () => - this.clearCtaHelpTimeout() - this.clearBandFilterHelpTimeout() - this.clearMasterHelpTimeout() - this.clearResults(); - - events:() => - @genre.on 'change', this.search - @artist.on 'change', this.search - @instrument.on 'change', this.search - @availability.on 'change', this.search - - clearResults:() => - @currentPage = 0 - @content.empty() - @noMoreJamtracks.hide() - @next = null - - startHelperBubbles: () => - @showBandFilterHelpTimeout = setTimeout(@showBandFilterHelp, 3500) - - showBandFilterHelp: () => - context.JK.HelpBubbleHelper.jamtrackBrowseBand(@artist.closest('.easydropdown-wrapper'), $('body')) - - @showMasterHelpDueTime = new Date().getTime() + 11000 # 6000 ms for band tooltip to display, and 5 seconds of quiet time - @scroller.on('scroll', @masterHelpScrollWatch) - @scroller.on('scroll', @clearBubbles) - @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime()) - - clearBubbles: () => - if @helpBubble? - @helpBubble.btOff() - @helpBubble = null - - # computes when we should show the master help bubble - masterHelpDueTime: () => - dueTime = @showMasterHelpDueTime - new Date().getTime() - if dueTime <= 0 - dueTime = 2000 - dueTime - - - # computes when we should show the master help bubble - ctaHelpDueTime: () => - dueTime = @showCtaHelpDueTime - new Date().getTime() - if dueTime <= 0 - dueTime = 2000 - dueTime - - # if the user scrolls, reset the master help due time - masterHelpScrollWatch: () => - @clearMasterHelpTimeout() - @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime() + 2000) - - # if the user scrolls, reset the master help due time - ctaHelpScrollWatch: () => - @clearCtaHelpTimeout() - @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime() + 2000) - - - showCtaHelp: () => - @scroller.off('scroll', @ctaHelpScrollWatch) - @clearCtaHelpTimeout() - - cutoff = @scroller.offset().top; - - @screen.find('.jamtrack-actions').each((i, element) => - $element = $(element) - - if ($element.offset().top >= cutoff) - @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseCta($element, $('body')) - return false - else - return true - ) - - showMasterHelp: () => - @scroller.off('scroll', @masterHelpScrollWatch) - @clearMasterHelpTimeout() - - # don't show the help if the user has already clicked a preview - unless @userPreviewed - cutoff = @scroller.offset().top; - - @screen.find('.jamtrack-preview[data-track-type="Master"]').each((i, element) => - $element = $(element) - - if ($element.offset().top >= cutoff) - @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseMasterMix($element.find('.play-button'), $('body')) - return false - else - return true - ) - - @showCtaHelpDueTime = new Date().getTime() + 11000 - @scroller.on('scroll', @ctaHelpScrollWatch) - @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime()) # 6000 ms for bubble show time, and 5000ms for delay - - previewPlayed: () => - @userPreviewed = true - - clearCtaHelpTimeout:() => - if @showCtaHelpTimeout? - clearTimeout(@showCtaHelpTimeout) - @showCtaHelpTimeout = null - - clearBandFilterHelpTimeout: () => - if @showBandFilterHelpTimeout? - clearTimeout(@showBandFilterHelpTimeout) - @showBandFilterHelpTimeout = null - - clearMasterHelpTimeout: () => - if @showMasterHelpTimeout? - clearTimeout(@showMasterHelpTimeout) - @showMasterHelpTimeout = null - - setFilterFromURL:() => - # Grab parms from URL for artist, instrument, and availability - parms=this.getParams() - - if(parms.artist?) - @artist.val(parms.artist) - else - @artist.val('') - if(parms.instrument?) - @instrument.val(parms.instrument) - else - @instrument.val('') - if(parms.availability?) - @availability.val(parms.availability) - else - @availability.val('') - - if window.history.replaceState #ie9 proofing - window.history.replaceState({}, "", "/client#/jamtrackBrowse") - - getParams:() => - params = {} - q = window.location.href.split("?")[1] - if q? - q = q.split('#')[0] - raw_vars = q.split("&") - for v in raw_vars - [key, val] = v.split("=") - params[key] = decodeURIComponent(val) - params - - setFilterState: (state) => - if state - @genre.easyDropDown('enable').removeAttr('disabled') - @artist.easyDropDown('enable').removeAttr('disabled') - @instrument.easyDropDown('enable').removeAttr('disabled') - @availability.easyDropDown('enable').removeAttr('disabled') - else - @genre.easyDropDown('disable').attr('disabled', 'disabled') - @artist.easyDropDown('disable').attr('disabled', 'disabled') - @instrument.easyDropDown('disable').attr('disabled', 'disabled') - @availability.easyDropDown('disable').attr('disabled', 'disabled') - - refresh:() => - this.clearResults() - @currentQuery = this.buildQuery() - that = this - this.setFilterState(false) - rest.getJamTracks(@currentQuery).done((response) => - that.handleJamtrackResponse(response) - ).fail( (jqXHR) => - that.clearResults() - that.noMoreJamtracks.show() - that.app.notifyServerError jqXHR, 'Jamtrack Unavailable' - ).always () => - that.setFilterState(true) - - search:() => - this.refresh() - false - - defaultQuery:() => - query = - per_page: LIMIT - page: @currentPage+1 - if @next - query.since = @next - query - - buildQuery:() => - @currentQuery = this.defaultQuery() - # genre filter - # var genres = @screen.find('#jamtrack_genre').val() - # if (genres !== undefined) { - # @currentQuery.genre = genres - # } - # instrument filter - - instrument = @instrument.val() - if instrument? - @currentQuery.instrument = instrument - - # artist filter - art = @artist.val() - if art? - @currentQuery.artist = art - - # availability filter - availability = @availability.val() - if availability? - @currentQuery.availability = availability - @currentQuery - - handleJamtrackResponse:(response) => - @next = response.next - this.renderJamtracks(response) - if response.next == null - # if we less results than asked for, end searching - @scroller.infinitescroll 'pause' - if @currentPage == 0 and response.jamtracks.length == 0 - @content.append '
Enter the code from the back of your gift card to associate it with your account.
` + else + form = + `` + 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? }) + `