diff --git a/admin/Gemfile b/admin/Gemfile index 89eaf90af..c66e1b869 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -36,11 +36,11 @@ gem 'coffee-script-source' #, '~> 1.4.0' # ADD THIS LINE, 1.5.0 doesn't compile gem 'devise' #, '3.3.0' gem 'will_paginate' #, '3.0.3' gem 'bootstrap-will_paginate', '0.0.6' -gem 'carrierwave' #, '0.9.0' +gem 'carrierwave', '0.11.2' #, '0.9.0' gem 'carrierwave_direct' gem 'uuidtools', '2.1.2' -gem 'jquery-ui-rails' #, '4.2.1' -gem 'jquery-rails' +gem 'jquery-ui-rails', '5.0.5' #, '4.2.1' +gem 'jquery-rails', '4.1.1' # both this and jquery-ui-rails are pinned; if you unpin, jquery/autocomplete is missing during precomplie gem 'rails-jquery-autocomplete' # This is the maintained version of rails3-jquery-autocomplete gem 'activeadmin' , '1.0.0.pre4'# github: 'activeadmin', branch: 'master' gem 'mime-types', '1.25' diff --git a/admin/app/admin/jamblaster.rb b/admin/app/admin/jamblaster.rb index d64666711..acaebe88d 100644 --- a/admin/app/admin/jamblaster.rb +++ b/admin/app/admin/jamblaster.rb @@ -1,8 +1,12 @@ -ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do +ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do + config.filters = false menu :label => 'JamBlasters', :parent => 'JamBlaster' + scope("Connected", default: true) { |scope| scope.where('client_id in (select client_id from connections)') } + scope("All") { |scope| scope.order('created_at desc') } + form do |f| f.inputs 'New JamBlaster' do f.input :user, required: true, collection: User.all, include_blank: false @@ -12,4 +16,25 @@ ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do end f.actions end + + + index do + + # actions # use this for all view/edit/delete links + + column 'Serial' do |oo| + oo.serial_no + end + + column 'IPv4' do |oo| + oo.ipv4_link_local + end + + column 'IPv6' do |oo| + oo.ipv6_link_local + end + + end + + end diff --git a/admin/app/admin/sale_line_items.rb b/admin/app/admin/sale_line_items.rb index c64862611..e691f535c 100644 --- a/admin/app/admin/sale_line_items.rb +++ b/admin/app/admin/sale_line_items.rb @@ -30,11 +30,17 @@ ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do end end column 'Source' do |oo| - oo.sale.source + if oo.sale.source == JamRuby::Sale::SOURCE_PAYPAL + link_to(oo.sale.source, 'https://history.paypal.com/webscr?cmd=_history-details-from-hub&id=' + oo.sale.recurly_invoice_id) + else + oo.sale.source + end end column 'When' do |oo| oo.created_at end + column 'Link' do |oo| + end end diff --git a/db/manifest b/db/manifest index 5a9ba3bbd..cf77ffe95 100755 --- a/db/manifest +++ b/db/manifest @@ -368,4 +368,8 @@ retailers.sql second_ed.sql second_ed_v2.sql retailers_v2.sql -retailer_interest.sql \ No newline at end of file +retailer_interest.sql +connection_role.sql +retailer_payment_split.sql +teacher_distribution_fields.sql +jam_track_download_rights.sql \ No newline at end of file diff --git a/db/up/connection_role.sql b/db/up/connection_role.sql new file mode 100644 index 000000000..754df6635 --- /dev/null +++ b/db/up/connection_role.sql @@ -0,0 +1,2 @@ +ALTER TABLE connections ADD COLUMN client_role VARCHAR; +ALTER TABLE connections ADD COLUMN parent_client_id VARCHAR; \ No newline at end of file diff --git a/db/up/jam_track_download_rights.sql b/db/up/jam_track_download_rights.sql new file mode 100644 index 000000000..422178858 --- /dev/null +++ b/db/up/jam_track_download_rights.sql @@ -0,0 +1,8 @@ +ALTER TABLE jam_tracks ADD COLUMN download_price numeric; +UPDATE jam_tracks SET download_price = 4.99; +ALTER TABLE jam_track_rights ADD COLUMN can_download BOOLEAN NOT NULL DEFAULT FALSE; +UPDATE jam_track_rights SET can_download = TRUE; +ALTER TABLE shopping_carts ADD COLUMN variant VARCHAR; +UPDATE shopping_carts set variant = 'stream' where cart_type = 'JamTrack'; +ALTER TABLE sale_line_items ADD COLUMN variant VARCHAR; +UPDATE sale_line_items set variant = 'full'; \ No newline at end of file diff --git a/db/up/retailer_payment_split.sql b/db/up/retailer_payment_split.sql new file mode 100644 index 000000000..336beee4d --- /dev/null +++ b/db/up/retailer_payment_split.sql @@ -0,0 +1,2 @@ +ALTER TABLE retailers ADD COLUMN payment VARCHAR; +ALTER TABLE lesson_bookings ADD COLUMN payment VARCHAR; \ No newline at end of file diff --git a/db/up/teacher_distribution_fields.sql b/db/up/teacher_distribution_fields.sql new file mode 100644 index 000000000..6401a9008 --- /dev/null +++ b/db/up/teacher_distribution_fields.sql @@ -0,0 +1,3 @@ +ALTER TABLE teacher_distributions ADD COLUMN teacher_fee_in_cents INTEGER; +ALTER TABLE lesson_bookings ADD COLUMN same_retailer BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN admin_marked BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/ruby/Gemfile b/ruby/Gemfile index e1b13151c..c716c1666 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -64,6 +64,7 @@ gem 'icalendar' gem 'email_validator' group :test do + gem 'pry' gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' gem 'factory_girl', '4.5.0' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 4afe09eb5..cd051e8bb 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -40,6 +40,7 @@ require "jam_ruby/errors/state_error" require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/jam_record_not_found" require "jam_ruby/errors/conflict_error" +require "jam_ruby/errors/pay_pal_client_error" require "jam_ruby/lib/app_config" require "jam_ruby/lib/s3_manager_mixin" require "jam_ruby/lib/s3_public_manager_mixin" diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb index 8607f7bd5..86f30742f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb @@ -42,7 +42,7 @@ yourself playing along with the rest of the band in audio or video, and more. Get your first - JamTrack free to try one out! After that they are just $1.99 each. Click here for more + JamTrack free to try one out! After that they are just $1.99/$4.99 each. Click here for more information on how you can use JamTracks in your browser, in our free Mac or Windows diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 92d960b22..92aea6228 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -402,7 +402,7 @@ SQL music_session.creator end - def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil) + def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, client_role = nil, parent_client_id = nil, video_sources=nil) connection = nil ConnectionManager.active_record_transaction do |connection_manager| @@ -418,7 +418,7 @@ SQL raise JamPermissionError, "wrong user_id associated with connection #{client_id}" end - connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, video_sources) + connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, client_role, parent_client_id, video_sources) JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id, client_id) # connection.music_session_id = music_session.id # connection.as_musician = as_musician diff --git a/ruby/lib/jam_ruby/errors/pay_pal_client_error.rb b/ruby/lib/jam_ruby/errors/pay_pal_client_error.rb new file mode 100644 index 000000000..faf3159f4 --- /dev/null +++ b/ruby/lib/jam_ruby/errors/pay_pal_client_error.rb @@ -0,0 +1,19 @@ +module JamRuby + class PayPalClientError < StandardError + + attr_accessor :errors + def initialize(data) + if data.respond_to?('has_key?') + self.errors = data + else + self.errors = {:message=>data.to_s} + end + end # initialize + + def to_s + s=super + s << ", errors: #{errors.inspect}" if self.errors.any? + s + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 671e0dba0..4e9fb0a0e 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -868,6 +868,7 @@ module JamRuby jam_track.genres = determine_genres(metadata) jam_track.language = determine_language(metadata) jam_track.price = 1.99 + jam_track.download_price = 4.99 jam_track.reproduction_royalty_amount = nil jam_track.reproduction_royalty = true jam_track.public_performance_royalty = true diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index dd809d35f..41a8ebfa5 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -501,7 +501,7 @@ module JamRuby [music_sessions, user_scores] end - def self.participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency, video_sources=nil) + def self.participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency, client_role = nil, parent_client_id = nil, video_sources=nil) music_session = MusicSession.find(music_session_id) # USERS ARE ALREADY IN SESSION @@ -514,7 +514,7 @@ module JamRuby active_music_session.with_lock do # VRFS-1297 active_music_session.tick_track_changes # VRFS-3986 - connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) + connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, client_role, parent_client_id, video_sources) if connection.errors.any? # rollback the transaction to make sure nothing is disturbed in the database @@ -573,7 +573,7 @@ module JamRuby # auto-join this user into the newly created session as_musician = true - connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) + connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, client_role, parent_client_id, video_sources) unless connection.errors.any? user.update_progression_field(:first_music_session_at) diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 18f250616..8bd75a4a6 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -176,11 +176,13 @@ module JamRuby true end - def join_the_session(music_session, as_musician, tracks, user, audio_latency, videos=nil) + def join_the_session(music_session, as_musician, tracks, user, audio_latency, client_role = nil, parent_client_id = nil, videos=nil) self.music_session_id = music_session.id self.as_musician = as_musician == true # this is deliberate; otherwise we create a warning in one our tests that passes 'blarg' (rails warning about casting strings to false) self.joining_session = true self.joined_session_at = Time.now + self.client_role = client_role + self.parent_client_id = parent_client_id associate_tracks(tracks) unless tracks.nil? associate_videos(videos) unless videos.nil? self.save diff --git a/ruby/lib/jam_ruby/models/gift_card_type.rb b/ruby/lib/jam_ruby/models/gift_card_type.rb index 8294dbfed..192a9438e 100644 --- a/ruby/lib/jam_ruby/models/gift_card_type.rb +++ b/ruby/lib/jam_ruby/models/gift_card_type.rb @@ -28,6 +28,10 @@ module JamRuby sale_display end + def variant_price(variant = nil) + price + end + def price if card_type == JAM_TRACKS_5 10.00 @@ -39,7 +43,7 @@ module JamRuby end - def sale_display + def sale_display(variant = nil) if card_type == JAM_TRACKS_5 'JamTracks Gift Card (5)' elsif card_type == JAM_TRACKS_10 diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index dd3d3fdcc..d70e4e8c1 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -161,9 +161,22 @@ module JamRuby true end - def sale_display - "JamTrack: " + name + def sale_display(variant = nil) + if variant == ShoppingCart::JAMTRACK_FULL + variant_desc = 'FULL' + elsif variant == ShoppingCart::JAMTRACK_DOWNLOAD + variant_desc = 'UPRGADE' + elsif variant == ShoppingCart::JAMTRACK_STREAM + variant_desc = 'FOR USE ONLY WITHIN APP' + else + variant_desc = 'UNKNOWN' + end + + + "JamTrack: #{name} - #{variant_desc}" end + + def duplicate_positions? counter = {} jam_track_tracks.each do |track| @@ -504,14 +517,43 @@ module JamRuby owners.include?(user) end - def right_for_user(user) - jam_track_rights.where("user_id=?", user).first + def right_for_user(user, variant = nil) + + query = jam_track_rights.where("user_id=?", user) + + if variant + if variant == ShoppingCart::JAMTRACK_DOWNLOAD + query = query.where('can_download', true) + elsif variant == ShoppingCart::JAMTRACK_FULL + query = query.where('can_download', true) + elsif variant == ShoppingCart::JAMTRACK_STREAM + + else + throw 'unknown variant ' + variant + end + end + query.first end + def mixdowns_for_user(user) JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id) end + def upgrade_price + variant_price('download') + end + + def variant_price(variant) + if variant == 'full' + download_price + elsif variant == 'download' + download_price - price + else + price + end + end + def short_plan_code prefix = 'jamtrack-' plan_code[prefix.length..-1] diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 8a2688f06..6570c1f99 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -45,6 +45,7 @@ module JamRuby belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy belongs_to :school, class_name: "JamRuby::School" + belongs_to :retailer, class_name: "JamRuby::Retailer" belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice" belongs_to :posa_card, class_name: "JamRuby::PosaCard" has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy @@ -527,11 +528,15 @@ module JamRuby end end - def distribution_price_in_cents(target, education) + def distribution_price_in_cents(target, education, split = nil, fee_rate = nil) distribution = teacher_distribution_price_in_cents(target) - if education - (distribution * 0.0625).round + if split + (distribution * split).round + + # when a split is provided, we also pin down the teacher_fee_in_cents, instead of assuming a bunch of stuff + elsif education + (distribution * 0.0625).round # 0.0625 is 1/4th of 25% else distribution end @@ -799,6 +804,13 @@ module JamRuby lesson_booking.school = lesson_booking.teacher.teacher.school end + # copy payment settings from retailer into lesson booking + if lesson_booking.teacher && lesson_booking.teacher.teacher.retailer + lesson_booking.retailer = lesson_booking.teacher.teacher.retailer + lesson_booking.payment = lesson_booking.teacher.teacher.retailer.payment_details.to_json + lesson_booking.same_retailer = lesson_booking.teacher.teacher.retailer.affiliate_partner == user.affiliate_referral + end + if user lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id)) if lesson_booking.same_school diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index dd3aa914e..80ec5c670 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -53,11 +53,11 @@ module JamRuby end def teacher_distribution - teacher_distributions.where(education:false).first + teacher_distributions.where(education: false).first end def education_distribution - teacher_distributions.where(education:true).first + teacher_distributions.where(education: true).first end def add_test_drives @@ -97,15 +97,46 @@ module JamRuby purchase.month = month purchase.recurring = true + # this is for monthly if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) - teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false) - purchase.teacher_distributions << teacher_dist - # price should always match the teacher_distribution, if there is one - purchase.price = teacher_dist.amount_in_cents / 100 - if lesson_booking.school_on_school_payment? - purchase.teacher_distributions << TeacherDistribution.create_for_lesson_package_purchase(purchase, true) + if lesson_booking.posa_card.nil? && lesson_booking.payment + + # if there is a payment object, it will describe how everything gets doled out + payment = JSON.parse(lesson_booking.payment) + + teacher_split = payment["teacher"] + retailer_split = payment["retailer"] + + retailer_rate = teacher.teacher.retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] # add 0.03 to account for stripe deduction + + + if teacher_split && teacher_split > 0 + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false, (teacher_split / 100.0).round(2), retailer_rate) + purchase.teacher_distributions << teacher_dist + # price should always match the teacher_distribution, if there is one + purchase.price = teacher_dist.amount_in_cents / 100 + end + + if retailer_split && retailer_split > 0 + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false, (retailer_split / 100.0).round(2), retailer_rate) + teacher_dist.retailer = teacher.teacher.retailer + teacher_dist.save + purchase.teacher_distributions << teacher_dist + end + + else + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false) + purchase.teacher_distributions << teacher_dist + # price should always match the teacher_distribution, if there is one + purchase.price = teacher_dist.amount_in_cents / 100 + + if lesson_booking.school_on_school_payment? + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, true) + purchase.teacher_distributions << teacher_dist + end + end end else diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb index 196e67ccd..5424763fd 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_type.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -102,10 +102,13 @@ module JamRuby end - def sale_display + def sale_display(variant = nil) name end + def variant_price(variant = nil) + price + end def plan_code if package_type == SINGLE_FREE "lesson-package-single-free" diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index f0b3bbb2f..2051d9768 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -97,13 +97,17 @@ module JamRuby end def teacher_distribution - teacher_distributions.where(education:false).first + teacher_distributions.where(education:false).where('retailer_id is null').first end def education_distribution teacher_distributions.where(education:true).first end + def retailer_distribution + teacher_distributions.where('retailer_id is not null').first + end + def manage_slot_changes # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . # TODO: what to do, what to do. @@ -197,6 +201,59 @@ module JamRuby counterer_id.nil? || counterer_id == student_id end + def mark_lesson(success, administratively_marked = false) + self.success = success + self.analysed_at = Time.now + self.analysed = true + self.status = STATUS_COMPLETED + self.admin_marked = administratively_marked + + + if success && lesson_booking.requires_teacher_distribution?(self) + + if lesson_booking.posa_card.nil? && lesson_booking.payment + # if there is a payment object, it will describe how everything gets doled out + payment = JSON.parse(lesson_booking.payment) + + teacher_split = payment["teacher"] + retailer_split = payment["retailer"] + + retailer_rate = teacher.teacher.retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] # add 0.03 to account for stripe deduction + + # but we must also take the correct rate out of the + + if is_test_drive? + # test drives do not get subject to split behavior, per quick convo with David + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) + else + + if teacher_split && teacher_split > 0 + teacher_dist = TeacherDistribution.create_for_lesson(self, false, (teacher_split / 100.0).round(2), retailer_rate) + self.teacher_distributions << teacher_dist + end + + if retailer_split && retailer_split > 0 + teacher_dist = TeacherDistribution.create_for_lesson(self, false, (retailer_split / 100.0).round(2), retailer_rate) + teacher_dist.retailer = teacher.teacher.retailer + teacher_dist.save + end + end + else + is_education_school_on_school = lesson_booking.school_on_school_payment? + + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) + if is_education_school_on_school + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true) + end + end + end + + if self.save + # send out emails appropriate for this type of session + session_completed + end + end + def analyse if self.analysed return @@ -210,23 +267,8 @@ module JamRuby # extra protection against bad code somewhere return end - self.success = analysis[:bill] - self.analysed_at = Time.now - self.analysed = true - self.status = STATUS_COMPLETED - - if success && lesson_booking.requires_teacher_distribution?(self) - self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) - if lesson_booking.school_on_school_payment? - self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true) - end - end - - if self.save - # send out emails appropriate for this type of session - session_completed - end + mark_lesson(analysis[:bill]) end def billed diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index d55ebf1ad..79f85a392 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -948,6 +948,9 @@ SQL if scheduled_duration.class == String duration = scheduled_duration.to_i.seconds end + if duration == 0 + duration = 30 * 60 + end duration end @@ -996,7 +999,7 @@ SQL "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" end else - "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" + "#{start_time.strftime("%A, %B %e")} - #{end_time.strftime("%l:%M%P").strip}" end else diff --git a/ruby/lib/jam_ruby/models/posa_card_type.rb b/ruby/lib/jam_ruby/models/posa_card_type.rb index fb2c70e78..570442609 100644 --- a/ruby/lib/jam_ruby/models/posa_card_type.rb +++ b/ruby/lib/jam_ruby/models/posa_card_type.rb @@ -36,6 +36,10 @@ module JamRuby sale_display end + def variant_price(variant = nil) + price + end + def price if card_type == JAM_TRACKS_5 10.00 @@ -49,7 +53,7 @@ module JamRuby end - def sale_display + def sale_display(variant = nil) if card_type == JAM_TRACKS_5 'JamTracks Card (5)' elsif card_type == JAM_TRACKS_10 diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 9c87bf6f8..dbfd2e01e 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -272,7 +272,9 @@ module JamRuby music_session.connections.each do |connection| connection.tracks.each do |track| - recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) + if connection.client_role != 'child' + recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) + end end connection.video_sources.each do |video| diff --git a/ruby/lib/jam_ruby/models/retailer.rb b/ruby/lib/jam_ruby/models/retailer.rb index 50185e5eb..1b5d37e6c 100644 --- a/ruby/lib/jam_ruby/models/retailer.rb +++ b/ruby/lib/jam_ruby/models/retailer.rb @@ -16,6 +16,7 @@ module JamRuby has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' has_many :sales, class_name: 'JamRuby::Sale' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' + has_many :lesson_bookings, class_name: 'JamRuby::LessonBooking' validates :user, presence: true #validates :slug, presence: true @@ -60,11 +61,23 @@ module JamRuby BCrypt::Password.new(self.encrypted_password) == password end + def update_payment(split) + if split[:teacher] && split[:teacher].is_a?(Integer) && split[:retailer] && split[:retailer].is_a?(Integer) + teacher_split = split[:teacher] + retailer_split = split[:retailer] + if (teacher_split >= 0 && teacher_split <= 100) && (retailer_split >= 0 && retailer_split <= 100) && (teacher_split + retailer_split == 100) + self.payment = split.to_json + end + end + end def update_from_params(params) self.name = params[:name] if params[:name].present? self.city = params[:city] self.state = params[:state] self.slug = params[:slug] if params[:slug].present? + if params[:split] + self.update_payment(params[:split]) + end if params[:password].present? self.should_validate_password = true @@ -74,6 +87,15 @@ module JamRuby self.save end + # should be of form {teacher: 0-100, retailer: 0-100} + def payment_details + if self.payment + JSON.parse(self.payment) + else + {"teacher" =>100, "retailer" => 0} + end + end + def owner user end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 5f6ff9c05..97a633e90 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -9,6 +9,7 @@ module JamRuby SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' + SOURCE_PAYPAL = 'paypal' belongs_to :retailer, class_name: 'JamRuby::Retailer' belongs_to :user, class_name: 'JamRuby::User' @@ -117,8 +118,24 @@ module JamRuby price_info end - def self.ios_purchase(current_user, jam_track, receipt, price_data) - jam_track_right = nil + def self.ios_purchase(current_user, jam_track, receipt, price_data, variant) + + if variant.nil? + variant = ShoppingCart::JAMTRACK_STREAM + end + + # see if we should bail because we already own these rights + jam_track_right = jam_track.right_for_user(current_user) + + if !jam_track_right.nil? && jam_track_right.can_download + # if the user already has full rights to the JamTrack, there is nothing else to do in this path + return jam_track_right + end + + if !jam_track_right.nil? && (!jam_track_right.can_download && variant == ShoppingCart::JAMTRACK_STREAM) + # if the user does have the track, but isn't upgrading it, bail + return jam_track_right + end # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it Sale.transaction do @@ -157,6 +174,11 @@ module JamRuby jam_track_right.redeemed = using_free_credit jam_track_right.version = jam_track.version end + + if variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL + jam_track_right.can_download = true + jam_track_right.save + end end jam_track_right @@ -166,7 +188,7 @@ module JamRuby # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation) # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028) # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack - def self.place_order(current_user, shopping_carts) + def self.place_order(current_user, shopping_carts, paypal = false) sales = [] @@ -176,7 +198,7 @@ module JamRuby # return sales #end - jam_track_sale = order_jam_tracks(current_user, shopping_carts) + jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal) sales << jam_track_sale if jam_track_sale # TODO: process shopping_carts_subscriptions @@ -377,7 +399,7 @@ module JamRuby # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned - def self.order_jam_tracks(current_user, shopping_carts) + def self.order_jam_tracks(current_user, shopping_carts, is_paypal) shopping_carts_jam_tracks = [] shopping_carts_subscriptions = [] @@ -395,11 +417,9 @@ module JamRuby end end - client = RecurlyClient.new - sale = nil Sale.transaction do - sale = create_jam_track_sale(current_user, SOURCE_RECURLY) + sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY) if sale.valid? if is_only_freebie(shopping_carts) @@ -429,61 +449,142 @@ module JamRuby else - account = client.get_account(current_user) - if account.present? + if is_paypal - purge_pending_adjustments(account) + sale.process_shopping_carts(current_user, shopping_carts) - created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) + paypal_auth = current_user.paypal_auth - # now invoice the sale ... almost done + @api = PayPal::SDK::Merchant::API.new + @get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token}) + @response = @api.get_express_checkout_details(@get_express_checkout_details) - begin - invoice = account.invoice! - sale.recurly_invoice_id = invoice.uuid - sale.recurly_invoice_number = invoice.invoice_number + @@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}") + + tax = false + if @response.Ack == 'Success' + payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo + if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX') + # we need to ask for taxes + tax = true + end + end + + tax_rate = tax ? 0.0825 : 0 + total = current_user.shopping_cart_total.round(2) + tax_total = (total * tax_rate).round(2) + total = total + tax_total + total = total.round(2) + + @do_express_checkout_payment = @api.build_do_express_checkout_payment({ + :DoExpressCheckoutPaymentRequestDetails => { + :PaymentDetails => + [ + { + :OrderTotal => { + :currencyID => "USD", + :value => total + }, + :PaymentAction => "Sale" + } + ], + :Token => paypal_auth.token, + :PayerID => paypal_auth.uid, }}) + @pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment) + + @@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}") + + # #, + # @FeeAmount=#, + # @TaxAmount=#, + # @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible", + # @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#>], + # @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#]>> + + if @pay_response.Ack == 'Success' + details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0] + sale.recurly_invoice_id = details.TransactionID + sale.recurly_invoice_number = details.ReceiptID # now slap in all the real tax/purchase totals - sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents - sale.recurly_tax_in_cents = invoice.tax_in_cents - sale.recurly_total_in_cents = invoice.total_in_cents - sale.recurly_currency = invoice.currency - - # and resolve against sale_line_items - sale.sale_line_items.each do |sale_line_item| - found_line_item = false - invoice.line_items.each do |line_item| - if line_item.uuid == sale_line_item.recurly_adjustment_uuid - sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents - sale_line_item.recurly_total_in_cents =line_item.total_in_cents - sale_line_item.recurly_currency = line_item.currency - sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents - found_line_item = true - break - end - - end - - if !found_line_item - @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") - puts "CANT FIND LINE ITEM" - end - end + sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i + sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i + sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i + sale.recurly_currency = details.GrossAmount.currencyID unless sale.save - puts "WTF" - raise RecurlyClientError, "Invalid sale (at end)." + puts "Invalid sale (at end)." + raise PayPalClientError, "Invalid sale (at end)." end - rescue Recurly::Resource::Invalid => e - # this exception is thrown by invoice! if the invoice is invalid - sale.rollback_adjustments(current_user, created_adjustments) - sale = nil - raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic - rescue => e - puts "UNKNOWN E #{e}" + else + @@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}") + raise PayPalClientError, @pay_response.Errors[0].LongMessage end else - raise RecurlyClientError, "Could not find account to place order." + client = RecurlyClient.new + account = client.get_account(current_user) + if account.present? + + purge_pending_adjustments(account) + + created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) + + # now invoice the sale ... almost done + + begin + invoice = account.invoice! + sale.recurly_invoice_id = invoice.uuid + sale.recurly_invoice_number = invoice.invoice_number + + # now slap in all the real tax/purchase totals + sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents + sale.recurly_tax_in_cents = invoice.tax_in_cents + sale.recurly_total_in_cents = invoice.total_in_cents + sale.recurly_currency = invoice.currency + + # and resolve against sale_line_items + sale.sale_line_items.each do |sale_line_item| + found_line_item = false + invoice.line_items.each do |line_item| + if line_item.uuid == sale_line_item.recurly_adjustment_uuid + sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents + sale_line_item.recurly_total_in_cents =line_item.total_in_cents + sale_line_item.recurly_currency = line_item.currency + sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents + found_line_item = true + break + end + + end + + if !found_line_item + @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") + puts "CANT FIND LINE ITEM" + end + end + + unless sale.save + puts "Invalid sale (at end)." + raise RecurlyClientError, "Invalid sale (at end)." + end + rescue Recurly::Resource::Invalid => e + # this exception is thrown by invoice! if the invoice is invalid + sale.rollback_adjustments(current_user, created_adjustments) + sale = nil + raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic + rescue => e + puts "UNKNOWN E #{e}" + end + else + raise RecurlyClientError, "Could not find account to place order." + end end end else @@ -493,7 +594,7 @@ module JamRuby sale end - def process_shopping_carts(current_user, shopping_carts, account) + def process_shopping_carts(current_user, shopping_carts, account = nil) created_adjustments = [] @@ -515,7 +616,7 @@ module JamRuby end - def process_shopping_cart(current_user, shopping_cart, account, created_adjustments) + def process_shopping_cart(current_user, shopping_cart, recurly_account, created_adjustments) recurly_adjustment_uuid = nil recurly_adjustment_credit_uuid = nil @@ -527,7 +628,8 @@ module JamRuby if shopping_cart.is_jam_track? jam_track = cart_product - if jam_track.right_for_user(current_user) + + if jam_track.right_for_user(current_user, shopping_cart.variant) # if the user already owns the JamTrack, we should just skip this cart item, and destroy it # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart) @@ -536,14 +638,14 @@ module JamRuby end - if account + if recurly_account # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack adjustments = shopping_cart.create_adjustment_attributes(current_user) adjustments.each do |adjustment| # create the adjustment at Recurly (this may not look like it, but it is a REST API) - created_adjustment = account.adjustments.new(adjustment) + created_adjustment = recurly_account.adjustments.new(adjustment) created_adjustment.save # if the adjustment could not be made, bail @@ -583,6 +685,13 @@ module JamRuby jam_track_right.version = jam_track.version end + + # deal with variant behavior + if shopping_cart.purchasing_downloadable_rights? + jam_track_right.can_download = true + jam_track_right.save + end + # 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 diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index eb053e144..b7f45f53d 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -175,6 +175,7 @@ module JamRuby sale_line_item = SaleLineItem.new sale_line_item.product_type = shopping_cart.cart_type + sale_line_item.variant = shopping_cart.variant sale_line_item.unit_price = product_info[:price] sale_line_item.quantity = product_info[:quantity] sale_line_item.free = product_info[:marked_for_redeem] diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index d9bc80a87..035e4f559 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -10,6 +10,11 @@ module JamRuby PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT] + JAMTRACK_FULL = 'full' + JAMTRACK_STREAM = 'stream' + JAMTRACK_DOWNLOAD = 'download' + JAMTRACK_VARIANTS = ['full', 'stream', 'download'] + attr_accessible :quantity, :cart_type, :product_info attr_accessor :skip_mix_check @@ -22,13 +27,15 @@ module JamRuby validates :cart_type, presence: true validates :cart_class_name, presence: true validates :marked_for_redeem, numericality: {only_integer: true} + validates :variant, inclusion: {in: [nil, JAMTRACK_FULL, JAMTRACK_STREAM, JAMTRACK_DOWNLOAD]} + #validate :not_mixed default_scope { order('created_at DESC') } def product_info(instance = nil) product = self.cart_product - data = {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display, allow_free: allow_free(product)} unless product.nil? + data = {type: cart_type, name: product.name, price: product.variant_price(variant), product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display(variant), allow_free: allow_free(product)} unless product.nil? if data && instance data.merge!(instance.product_info) end @@ -37,12 +44,16 @@ module JamRuby # multiply quantity by price def total_price(product) - quantity * product.price + quantity * product.variant_price(variant) + end + + def purchasing_downloadable_rights? + is_jam_track? && (variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL) end # multiply (quantity - redeemable) by price def real_price(product) - (quantity - marked_for_redeem) * product.price + (quantity - marked_for_redeem) * product.variant_price(variant) end def allow_free(product) @@ -101,7 +112,7 @@ module JamRuby end end - def self.create user, product, quantity = 1, mark_redeem = false + def self.create(user, product, quantity = 1, mark_redeem = false, variant = nil) cart = ShoppingCart.new if user.is_a?(User) @@ -111,6 +122,14 @@ module JamRuby end cart.cart_type = product.class::PRODUCT_TYPE + + if cart.cart_type == JamTrack::PRODUCT_TYPE && variant.nil? + cart.variant = JAMTRACK_STREAM # default to jamtrack 'stream' + else + cart.variant = variant + end + + cart.cart_class_name = product.class.name cart.cart_id = product.id cart.quantity = quantity @@ -158,9 +177,9 @@ module JamRuby shopping_carts.each do |shopping_cart| if shopping_cart.is_jam_track? mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) - cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem, shopping_cart.variant) else - cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false) + cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false, shopping_cart.variant) end end @@ -201,8 +220,13 @@ module JamRuby end # adds a jam_track to cart, checking for promotions - def self.add_jam_track_to_cart(any_user, jam_track, clear:false) + def self.add_jam_track_to_cart(any_user, jam_track, variant = JAMTRACK_FULL) cart = nil + + if variant.nil? + variant = JAMTRACK_FULL + end + ShoppingCart.transaction do # if clear @@ -213,7 +237,7 @@ module JamRuby end mark_redeem = jam_track.allow_free ? ShoppingCart.user_has_redeemable_jam_track?(any_user) : false - cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) + cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem, variant) end any_user.reload cart diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 7833cf0f6..119b29151 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -43,26 +43,35 @@ module JamRuby end end - def self.create_for_lesson(lesson_session, for_education) - distribution = create(lesson_session, for_education) + def self.create_for_lesson(lesson_session, for_education, split = nil, fee_rate = nil) + distribution = create(lesson_session, for_education, split, fee_rate) distribution.lesson_session = lesson_session distribution.education = for_education + + # lock down the teacher_fee_in_cents + distribution.teacher_fee_in_cents = distribution.calculate_teacher_fee(split, fee_rate) + distribution end - def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education) - distribution = create(lesson_package_purchase, for_education) + def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education, split = nil, fee_rate = nil) + distribution = create(lesson_package_purchase, for_education, split, fee_rate) distribution.lesson_package_purchase = lesson_package_purchase distribution.education = for_education + + + # lock down the teacher_fee_in_cents + distribution.teacher_fee_in_cents = distribution.calculate_teacher_fee(split, fee_rate) + distribution end - def self.create(target, education) + def self.create(target, education, split, fee_rate) distribution = TeacherDistribution.new distribution.teacher = target.teacher distribution.ready = false distribution.distributed = false - distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education) + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education, split, fee_rate) distribution.school = target.lesson_booking.school distribution end @@ -111,23 +120,34 @@ module JamRuby (jamkazam_margin_in_cents / 100).round(2) end - def calculate_teacher_fee - if education - 0 + def calculate_teacher_fee(split = nil, fee_rate = nil) + if teacher_fee_in_cents + return teacher_fee_in_cents else - if is_test_drive? + if education 0 else - if school - # if school exists, use it's rate - rate = school.jamkazam_rate + if is_test_drive? + 0 else - # otherwise use the teacher's rate - rate = teacher.teacher.jamkazam_rate + + if fee_rate + rate = (fee_rate * split) # charge_Fee is already handled elsewhere + else + if school + # if school exists, use it's rate + rate = school.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] + else + # otherwise use the teacher's rate + rate = teacher.teacher.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] + end + end + + (amount_in_cents * rate).round end - (amount_in_cents * (rate + 0.03)).round # 0.03 is stripe fee that we include in cost of JK fee end end + end def student diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 785ee77a5..592ba99b7 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -93,7 +93,7 @@ module JamRuby payment.amount_in_cents = payment.teacher_distribution.amount_in_cents payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee - effective_in_cents = payment.amount_in_cents - payment.fee_in_cents + effective_in_cents = payment.real_distribution_in_cents if payment.teacher_payment_charge.nil? charge = TeacherPaymentCharge.new diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb index 4dff407ce..becdcfdf3 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -41,7 +41,8 @@ module JamRuby :currency => "usd", :customer => APP_CONFIG.stripe[:source_customer], :description => construct_description, - :metadata => metadata + :metadata => metadata, + :destination => teacher.teacher.stripe_account_id ) stripe_charge diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 894275058..65f231dfb 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1912,6 +1912,13 @@ module JamRuby stats end + def shopping_cart_total + total = 0 + shopping_carts.each do |shopping_cart| + total += shopping_cart.product_info[:total_price] + end + total + end def destroy_all_shopping_carts ShoppingCart.where("user_id=?", self).destroy_all end @@ -2053,6 +2060,15 @@ module JamRuby user_authorizations.where(provider: "stripe_connect").first end + def paypal_auth + user_authorizations.where(provider: 'paypal').first + end + + def has_paypal_auth? + auth = paypal_auth + auth && (!auth.token_expiration || auth.token_expiration > Time.now) + end + def has_stripe_connect? auth = stripe_auth auth && (!auth.token_expiration || auth.token_expiration > Time.now) diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 59803b48d..462a9aa45 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -784,6 +784,7 @@ FactoryGirl.define do sequence(:publisher) { |n| "publisher-#{n}" } sales_region 'United States' price 1.99 + download_price 4.99 reproduction_royalty true public_performance_royalty true reproduction_royalty_amount 0.999 diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb index 7bddf1f36..38bd44018 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -24,7 +24,7 @@ describe "Monthly Recurring Lesson Flow" do teacher.stripe_account_id = stripe_account1_id teacher.save! } - it "works" do + it "works normal" do # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on Timecop.travel(Date.new(2016, 3, 20)) @@ -173,7 +173,7 @@ describe "Monthly Recurring Lesson Flow" do booked_price = booking.booked_price prorated = booked_price / 2 - prorated_cents = (booked_price * 100).to_i + prorated_cents = (prorated * 100).to_i user.reload user.lesson_purchases.length.should eql 1 lesson_purchase = user.lesson_purchases[0] @@ -205,10 +205,10 @@ describe "Monthly Recurring Lesson Flow" do teacher_distribution.distributed.should be_true TeacherPayment.count.should eql 1 payment = TeacherPayment.first - payment.amount_in_cents.should eql 3000 - payment.fee_in_cents.should eql (3000 * 0.28).round - payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round - payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.amount_in_cents.should eql prorated_cents + payment.fee_in_cents.should eql (prorated_cents * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (payment.real_distribution_in_cents + payment.real_distribution_in_cents * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (prorated_cents * 0.28).round payment.teacher.should eql teacher_user payment.teacher_distribution.should eql teacher_distribution @@ -405,7 +405,7 @@ describe "Monthly Recurring Lesson Flow" do booked_price = booking.booked_price prorated = booked_price / 2 - prorated_cents = (booked_price * 100).to_i + prorated_cents = (prorated * 100).to_i user.reload user.lesson_purchases.length.should eql 1 lesson_purchase = user.lesson_purchases[0] @@ -441,16 +441,16 @@ describe "Monthly Recurring Lesson Flow" do teacher_distribution.distributed.should be_true TeacherPayment.count.should eql 2 payment = teacher_distribution.teacher_payment - payment.amount_in_cents.should eql 3000 - payment.fee_in_cents.should eql (3000 * 0.28).round - payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round - payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.amount_in_cents.should eql prorated_cents + payment.fee_in_cents.should eql (prorated_cents * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (payment.real_distribution_in_cents + payment.real_distribution_in_cents * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (prorated_cents * 0.28).round payment.teacher.should eql teacher_user payment.teacher_distribution.should eql teacher_distribution education_distribution.reload education_distribution.distributed.should be_true - education_amt = (3000 * 0.0625).round + education_amt = (prorated_cents * 0.0625).round payment = education_distribution.teacher_payment payment.amount_in_cents.should eql education_amt payment.fee_in_cents.should eql 0 diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb index 382001519..b11a1c31e 100644 --- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -15,6 +15,7 @@ describe "Normal Lesson Flow" do let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } let(:school) {FactoryGirl.create(:school)} + let(:retailer) {FactoryGirl.create(:retailer)} after {Timecop.return} @@ -459,6 +460,7 @@ describe "Normal Lesson Flow" do booking.card_presumed_ok.should be_false booking.user.should eql user user.unprocessed_normal_lesson.should be_nil + booking.same_school_free.should be_true booking.sent_notices.should be_true booking.booked_price.should eql 30.00 booking.is_requested?.should be_true @@ -609,8 +611,8 @@ describe "Normal Lesson Flow" do booking.school.should be_true booking.card_presumed_ok.should be_false booking.user.should eql user - booking.same_school_free.should be_true - user.unprocessed_normal_lesson.should be_nil + booking.same_school_free.should be_false + #user.unprocessed_normal_lesson.should be_nil booking.sent_notices.should be_false booking.booked_price.should eql 30.00 booking.is_requested?.should be_true @@ -793,15 +795,236 @@ describe "Normal Lesson Flow" do payment = teacher_distribution.teacher_payment payment.amount_in_cents.should eql 3000 payment.fee_in_cents.should eql (3000 * 0.28).round - payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round - payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql ((teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) + (teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql payment.teacher_payment_charge.fee_in_cents payment.teacher.should eql teacher_user payment.teacher_distribution.should eql teacher_distribution lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED lesson_session.lesson_booking.success.should be_true + end + + it "works (retailer on retailer)" do + + # make sure teacher can get payments + teacher.stripe_account_id = stripe_account1_id + retailer.user.stripe_account_id = stripe_account2_id + + # make sure can get stripe payments + + # get user and teacher into same retailer + + teacher_split = 70 + retailer_split = 30 + teacher_split_pct = (teacher_split / 100.0) + retailer_split_pct = (retailer_split / 100.0) + retailer.update_payment({teacher: teacher_split, retailer:retailer_split}) + retailer.save! + + user.affiliate_referral = retailer.affiliate_partner + user.save! + teacher.retailer = retailer + teacher.save! + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.retailer.should eql retailer + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.same_school_free.should be_false + booking.same_retailer.should be_true + #user.unprocessed_normal_lesson.should be_nil + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + booking.is_requested?.should be_true + booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + LessonPaymentCharge.count.should eql 1 + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be_true + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 1 + + LessonBooking.hourly_check + + lesson_session.reload + lesson_session.education_distribution.should be_nil + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql (3000 * teacher_split_pct).round + teacher_distribution.teacher_fee_in_cents.should eql (teacher_distribution.amount_in_cents * (teacher_split_pct * (retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]))).round + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + lesson_session.teacher_distributions.count.should eql 2 + retailer_distribution = lesson_session.retailer_distribution + retailer_distribution.amount_in_cents.should eql (3000 * retailer_split_pct).round + retailer_distribution.teacher_fee_in_cents.should eql (retailer_distribution.amount_in_cents * (retailer_split_pct * (retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]))).round + retailer_distribution.ready.should be_true + retailer_distribution.distributed.should be_false + + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + user.sales.length.should eql 1 + lesson_session.amount_charged.should eql 32.48 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be_true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 2 + + LessonPaymentCharge.count.should eql 1 + TeacherDistribution.count.should eql 2 + + + teacher_distribution.reload + teacher_distribution.distributed.should be_true + retailer_distribution.reload + retailer_distribution.distributed.should be_true + + retailer_amt = (3000 * retailer_split_pct).round + payment = retailer_distribution.teacher_payment + payment.amount_in_cents.should eql retailer_distribution.amount_in_cents + payment.fee_in_cents.should eql retailer_distribution.teacher_fee_in_cents + payment.teacher_payment_charge.amount_in_cents.should eql ((retailer_distribution.amount_in_cents - retailer_distribution.teacher_fee_in_cents) + (retailer_distribution.amount_in_cents - retailer_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql retailer_distribution.teacher_fee_in_cents + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql retailer_distribution + payment = teacher_distribution.teacher_payment + payment.amount_in_cents.should eql teacher_distribution.amount_in_cents + payment.fee_in_cents.should eql teacher_distribution.teacher_fee_in_cents + payment.teacher_payment_charge.amount_in_cents.should eql ((teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) + (teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql payment.teacher_payment_charge.fee_in_cents + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true end it "affiliate gets their cut" do diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb index 47b3cd0b8..7dd23fdb2 100644 --- a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -338,7 +338,7 @@ describe LessonBooking do slot.day_of_week = jan1.wday booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) - times = booking.predicted_times_for_month(next_year, 1) + times = booking.predicted_times_for_month(next_year, 1)[:times] times.length.should eql 5 times[0].to_date.should eql (jan1) times[1].to_date.should eql (Date.new(next_year, 1, 8)) @@ -357,7 +357,7 @@ describe LessonBooking do slot.day_of_week = jan1.wday booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) - times = booking.predicted_times_for_month(next_year, 1) + times = booking.predicted_times_for_month(next_year, 1)[:times] times.length.should eql 3 times[0].to_date.should eql (Date.new(next_year, 1, 15)) times[1].to_date.should eql (Date.new(next_year, 1, 22)) diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 154aadb67..9a1de6bd5 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -890,29 +890,30 @@ describe MusicSession do end end - describe "purgeable sessions " do + describe "purgeable sessions" do it 'selects unscheduled sessions past due date' do interval = MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE - dd = Time.now - (interval.to_i + 1).days - Timecop.travel(dd) msess1 = FactoryGirl.create(:music_session) + dd = Time.now - (interval.to_i + 1).days + Timecop.travel(dd) msess2 = FactoryGirl.create(:music_session) purging = MusicSession.purgeable_sessions expect(purging.size).to be(1) - expect(purging[0].id).to eq(msess1.id) + expect(purging[0].id).to eq(msess2.id) end it 'selects recurring and non-recurring sessions past due date' do [MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE, MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE_RECUR].each do |interval| + Timecop.return + msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now) dd = Time.now - (interval.to_i + 1).days Timecop.travel(dd) - msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now) msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now) purging = MusicSession.purgeable_sessions expect(purging.size).to be(1) - expect(purging[0].id).to eq(msess1.id) + expect(purging[0].id).to eq(msess2.id) MusicSession.delete_all end end diff --git a/ruby/spec/jam_ruby/models/retailer_spec.rb b/ruby/spec/jam_ruby/models/retailer_spec.rb index b0ca83c27..96f144b11 100644 --- a/ruby/spec/jam_ruby/models/retailer_spec.rb +++ b/ruby/spec/jam_ruby/models/retailer_spec.rb @@ -20,7 +20,6 @@ describe Retailer do end it "has correct associations" do retailer = FactoryGirl.create(:retailer) - retailer.slug.should eql retailer.id retailer.should eql retailer.user.owned_retailer diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index a0d8077d3..4c8b45dc8 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -486,7 +486,7 @@ describe Sale do purchase= adjustments[0] purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) - purchase.description.should eq("JamTrack: " + jamtrack.name) + purchase.description.should eq("JamTrack: " + jamtrack.name + '- FOR USE ONLY WITHIN APP') purchase.state.should eq('invoiced') purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index 15cfdfd2d..7ac7911be 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -32,11 +32,11 @@ describe ShoppingCart do end it "allows mix of free and not free stuff" do - cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) + 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_track, clear: true) + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart2.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(1) @@ -44,7 +44,7 @@ describe ShoppingCart do cart3.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(2) - cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) cart4.errors.any?.should be_false user.reload user.shopping_carts.length.should eq(3) @@ -70,12 +70,12 @@ 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 = 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(1) user.reload - cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) cart2.should_not be_nil cart2.errors.any?.should be_false cart2.marked_for_redeem.should eq(1) diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb index 4a68ff207..3778c3eda 100644 --- a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -265,7 +265,7 @@ describe TeacherPayment do teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.destination.should be_nil + charge.destination.should_not be_nil charge.amount.should eql 726 charge.application_fee.should be_nil end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 0a4c93c00..59a3fe93e 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -306,7 +306,8 @@ def app_config :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', :source_customer => 'cus_88Vp44SLnBWMXq', # seth@jamkazam.com in JamKazam-test account - :ach_pct => 0.008 + :ach_pct => 0.008, + :charge_fee => 0.03 } end def musician_count diff --git a/web/Gemfile b/web/Gemfile index ddc1cf308..f9a12477a 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -28,6 +28,8 @@ gem 'sprockets-rails', '2.3.2' gem 'non-stupid-digest-assets' #gem 'license_finder' gem 'pg_migrate', '0.1.14' +#gem 'paypal-sdk-rest' +gem 'paypal-sdk-merchant-jk', '1.118.1' gem 'kickbox' gem 'oj', '2.10.2' gem 'builder' @@ -70,7 +72,7 @@ gem 'filepicker-rails', '0.1.0' gem 'aws-sdk', '~> 1' gem 'aasm' #, '3.0.16' gem 'carmen' -gem 'carrierwave' #, '0.9.0' +gem 'carrierwave', '0.11.2' #, '0.9.0' gem 'carrierwave_direct' gem 'fog' #gem 'jquery-payment-rails', github: 'sethcall/jquery-payment-rails' @@ -163,7 +165,7 @@ end gem 'sass-rails' gem 'coffee-rails' gem 'uglifier' - +gem 'coffee-script-source', '1.11.1' group :test, :cucumber do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 778e6ce47..49c563dc0 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -27,7 +27,7 @@ //= require jquery.Jcrop //= require jquery.naturalsize //= require jquery.queryparams -//= require jquery.clipboard +//= require clipboard //= require jquery.timeago //= require jquery.easydropdown //= require jquery.scrollTo diff --git a/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 2402c0023..dbca1c4c2 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -65,6 +65,8 @@ return; } + console.log("GetLocalRecordingState", localResults) + $.each(claimedRecordings, function(index, claimedRecording) { var options = { diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index 97d746144..04c3a602d 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -10,6 +10,8 @@ var $dialog = null; var $saveVideoCheckbox = null var $uploadToYoutube = null + var timeout = null + var CLIENT_ROLE = context.JK.CLIENT_ROLE function resetForm() { // remove all display errors @@ -44,6 +46,47 @@ resetForm(); + if(context.jamClient.getClientParentChildRole() == CLIENT_ROLE.CHILD) { + + logger.debug("child client; launching preview after xfer"); + $('#recording-finished-dialog span.nowait').addClass('hidden') + $('#recording-finished-dialog span.pleasewait').removeClass('hidden') + $('#recording-finished-dialog .preview-area').css('visibility', 'hidden') + $('#recording-finished-dialog form').css('visibility', 'hidden') + waitForMixTransfer() + } + else { + console.log("normal client; launching preview immediately") + $('#recording-finished-dialog span.pleasewait').addClass('hidden') + $('#recording-finished-dialog span.nowait').removeClass('hidden') + $('#recording-finished-dialog .preview-area').css('visibility', 'visible') + $('#recording-finished-dialog form').css('visibility', 'visible') + launchPreview(); + } + + } + + + function waitForMixTransfer() { + timeout = setTimeout(function() { + console.log("checking for file transfer", window.RecordingStore.mixTransferred) + + if(window.RecordingStore.mixTransferred) { + $('#recording-finished-dialog span.pleasewait').addClass('hidden') + $('#recording-finished-dialog span.nowait').removeClass('hidden') + $('#recording-finished-dialog .preview-area').css('visibility', 'visible') + $('#recording-finished-dialog form').css('visibility', 'visible') + timeout = null + launchPreview() + } + else { + waitForMixTransfer(); + } + + }, 1000) + } + + function launchPreview() { var parentSelector = '#recording-finished-dialog div.genre-selector'; context.JK.GenreSelectorHelper.render(parentSelector); @@ -117,10 +160,12 @@ playbackControls.startMonitor(); } } - } - function afterHide() { + if(timeout) { + clearTimeout(timeout) + timeout = null + } if(recording && recording.video) { var name = $('#recording-finished-dialog form input[name=name]').val(); name = name.replace(/[^A-Za-z0-9\-\ ]/g, ''); diff --git a/web/app/assets/javascripts/dialog/shareDialog.js b/web/app/assets/javascripts/dialog/shareDialog.js index 44d031682..b6aac6ae0 100644 --- a/web/app/assets/javascripts/dialog/shareDialog.js +++ b/web/app/assets/javascripts/dialog/shareDialog.js @@ -11,6 +11,7 @@ var userDetail = null; var entity = null; var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space + var clipboard = null; function showSpinner() { $(dialogId + ' .dialog-inner').hide(); @@ -444,27 +445,6 @@ function afterShow() { $("#shareType").text(entityType); - if(context.JK.hasFlash()) { - $("#btn-share-copy").clipboard({ - path: '/assets/jquery.clipboard.swf', - copy: function() { - // Return text in closest element (useful when you have multiple boxes that can be copied) - return $(".link-contents").text(); - } - }); - } - else { - if(context.jamClient) { - // uses bridge call to ultimately access QClipboard - $("#btn-share-copy").unbind('click').click(function() { - context.jamClient.SaveToClipboard($(".link-contents").text()); - return false; - }) - } - else { - logger.debug("no copy-to-clipboard capabilities") - } - } } function afterHide() { @@ -486,6 +466,20 @@ //initDialog(); facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); }); + + if(context.jamClient.IsNativeClient()) { + $("#btn-share-copy").unbind('click').click(function() { + context.jamClient.SaveToClipboard($("#link-contents").text()); + return false; + }) + } + else { + clipboard = new Clipboard('#btn-share-copy', { + text: function(trigger) { + return $("#link-contents").text(); + } + }) + } } this.initialize = initialize; diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index ae5713c90..4ada2663c 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -188,6 +188,37 @@ return context.JK.prodBubble($element, 'teacher-profile', {}, bigHelpDarkOptions({spikeGirth:0, spikeLength: 0, duration:10000, offsetParent:$offsetParent, width:385, positions:['top', 'right', 'bottom']})) } + helpBubble.jamtrackVariants = function($element, $offsetParent) { + var offer = function() { + console.log("jamtrackVariant turn off") + $element.btOff() + $offsetParent.off('click', offer) + } + + var bubble = context.JK.prodBubble($element, 'jamtrack-variants', {}, bigHelpDarkOptions({clickAnywhereToClose: true, spikeGirth:0, spikeLength: 0, duration:20000, positions:['bottom', 'right', 'left'], offsetParent: $offsetParent})) + setTimeout(function() { + $offsetParent.on('click', offer) + }, 1) + + return bubble + } + + helpBubble.jamtrackUpgrade = function($element, $offsetParent) { + var offer = function() { + console.log("jamtrackUpgrade turn off") + $element.btOff() + $offsetParent.off('click', offer) + } + + var bubble = context.JK.prodBubble($element, 'jamtrack-upgrade', {}, bigHelpDarkOptions({clickAnywhereToClose: true, spikeGirth:0, spikeLength: 0, duration:20000, positions:['bottom', 'right', 'left'], offsetParent: $offsetParent})) + setTimeout(function() { + $offsetParent.on('click', offer) + }, 1) + + return bubble + } + + helpBubble.showUseRemainingTestDrives = function($element, $offsetParent, user, callback) { return context.JK.onceBubble($element, 'side-remaining-test-drives', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 4e74551d1..a206635b9 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2734,6 +2734,27 @@ }) } + function paypalDetail(options) { + options = options || {} + return $.ajax({ + type: 'POST', + url: '/api/paypal/checkout/detail', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function paypalPlaceOrder(options) { + options = options || {} + return $.ajax({ + type: 'POST', + url: '/api/paypal/checkout/confirm', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(options) + }) + } function initialize() { return self; @@ -2977,6 +2998,8 @@ this.posaActivate = posaActivate; this.posaClaim = posaClaim; this.sendRetailerCustomerEmail = sendRetailerCustomerEmail; + this.paypalDetail = paypalDetail; + this.paypalPlaceOrder = paypalPlaceOrder; return this; }; })(window,jQuery); diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js index 58d0fa6f2..0410ed9c3 100644 --- a/web/app/assets/javascripts/landing/landing.js +++ b/web/app/assets/javascripts/landing/landing.js @@ -9,7 +9,7 @@ //= require jquery.queryparams //= require jquery.hoverIntent //= require jquery.cookie -//= require jquery.clipboard +//= require clipboard //= require jquery.easydropdown //= require jquery.carousel-1.1 //= require jquery.mousewheel-3.1.9 diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index 1ad2b3aea..43f8c02d3 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -496,7 +496,10 @@ monitoring = false; logger.debug("playbackControl.stopMonitor") if (monitorPlaybackTimeout != null) { - clearTimeout(monitorPlaybackTimeout); + if(clearTimeout) { + clearTimeout(monitorPlaybackTimeout); + } + monitorPlaybackTimeout = null; } } diff --git a/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee index e4d31e1be..315a40799 100644 --- a/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee @@ -92,6 +92,7 @@ profileUtils = context.JK.ProfileUtils selected: 'account', updateErrors: null, retailerName: null, + teacherSplit: null, teacherInvitations: null, updating: false } @@ -121,18 +122,25 @@ profileUtils = context.JK.ProfileUtils city = @root.find('select[name="cities"]').val() password = @root.find('input[type="password"]').val() + teacherSplit = @teacherSplit() + + if teacherSplit + retailerSplit = Number((100 - teacherSplit).toFixed(2)) + @setState(updating: true) rest.updateRetailer({ id: this.state.retailer.id, name: name, state: region, city: city, - password:password + password:password, + split: {teacher: teacherSplit, retailer: retailerSplit} }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + onUpdateDone: (response) -> - @setState({retailer: response, retailerName: null, updateErrors: null, updating: false}) + @setState({retailer: response, retailerName: null, teacherSplit: null, updateErrors: null, updating: false}) @app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"}) @@ -262,6 +270,63 @@ profileUtils = context.JK.ProfileUtils logger.debug("handleLocationChange #{country} #{region} ${city}") @setState({city: city, region: region}) + teacherSplitCurrent: () -> + if this.state.teacherSplit? + console.log("taking state for teacher split") + this.state.teacherSplit + else + this.state.retailer.payment_details.teacher + + teacherSplitValue: () -> + @teacherSplitCurrent() + + retailerSplitValue: () -> + teacherSplit = @teacherSplitCurrent() + return (100 - teacherSplit).toFixed(2) + + onTeacherBlur: () -> + teacherSplit = @root.find('input[name="teacher-split"]').val() + teacherSplit = Number(teacherSplit) + if teacherSplit != teacherSplit #NaN? + @setState({teacherSplit: null}) + + + teacherSplit: () -> + teacherSplit = @root.find('input[name="teacher-split"]').val() + if teacherSplit + teacherSplit = Number(teacherSplit) + if !teacherSplit + console.log("defaulting to 100 because teachersplit is empty") + teacherSplit = 100 + teacherSplit + + onTeacherSplitChange: (e) -> + $target = $(e.target) + + # edge cases first + teacherSplit = @root.find('input[name="teacher-split"]').val() + + if teacherSplit == null || teacherSplit == '' + @setState({teacherSplit: ''}) + return + + teacherSplit = Number(teacherSplit) + + if teacherSplit != teacherSplit # NaN? + console.log("teacher split is NaN; ignoring") + # do nothing; this way junk doesn't start showing up in retail square. Onblur will fix + return + + teacherSplit = @teacherSplit() + + if teacherSplit > 100 + console.log("teacher split is > 100. setting to 100") + return + if teacherSplit < 0 + console.log("teacher split is < 0. setting to 0") + return + @setState({teacherSplit: teacherSplit}) + account: () -> nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) @@ -313,6 +378,19 @@ profileUtils = context.JK.ProfileUtils +
+
+ + + Enter 0-100 +
+
+ + + This is computed automatically based on the Teacher % +
+
+
CANCEL UPDATE diff --git a/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee index 1a62c0ce1..b6467f523 100644 --- a/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/JamBlasterScreen.js.jsx.coffee @@ -249,7 +249,7 @@ JamBlasterActions = @JamBlasterActions audio: () -> `
- + To edit the JamBlaster audio settings, get into a session, and click the Settings link under My Tracks.
` ipSettingsChanged: (key, e) -> diff --git a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee index dd0a7227d..a3062a088 100644 --- a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee @@ -194,13 +194,14 @@ LessonTimerActions = context.LessonTimerActions @app.ajaxError(jqXHR) cancelSelected: (lesson, recurring) -> - rest.checkLessonCancel({id: lesson.id, update_all: recurring}).done((response) => (@issueCancelLesson(lesson, - recurring))).fail((jqXHR) => (@cancelSelectedFail(jqXHR))) + rest.checkLessonCancel({id: lesson.id, update_all: recurring}) + .done((response) => @issueCancelLesson(lesson, recurring)) + .fail((jqXHR) => @cancelSelectedFail(jqXHR, lesson)) - cancelSelectedFailed: (jqXHR) -> + cancelSelectedFail: (jqXHR, lesson) -> if jqXHR.status == 422 - if recurring + if lesson.recurring if @viewerStudent() buttons = [] buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) diff --git a/web/app/assets/javascripts/react-components/JamTrackFilterScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackFilterScreen.js.jsx.coffee index 15c26b9d7..d759547a7 100644 --- a/web/app/assets/javascripts/react-components/JamTrackFilterScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/JamTrackFilterScreen.js.jsx.coffee @@ -73,13 +73,37 @@ MIX_MODES = context.JK.MIX_MODES actionBtn = null if jamtrack.purchased - actionBtn = `PURCHASED` + if jamtrack.can_download + actionBtn = `PURCHASED` + else + priceNotice = `
$ {Number(jamtrack.upgrade_price).toFixed(2)}
` + actionBtn = `
+
+ {priceNotice} + UPGRADE TO FULL +
+ HELP +
` + + else if jamtrack.is_free - actionBtn = `GET IT FREE!` + actionBtn = `GET IT FREE!` else if jamtrack.added_cart actionBtn = `ALREADY IN CART` else - actionBtn = `ADD TO CART` + priceNotice = `
$ {jamtrack.price}
` + fullPriceNotice = `
$ {jamtrack.download_price}
` + actionBtn = `
+
+ {priceNotice} + ADD TO CART +
+
+ {fullPriceNotice} + ADD TO CART (FULL) +
+ HELP +
` availabilityNotice = null if jamtrack.sales_region==context.JK.AVAILABILITY_US @@ -114,7 +138,6 @@ MIX_MODES = context.JK.MIX_MODES
-
$ {jamtrack.price}
{actionBtn} {availabilityNotice}
@@ -213,7 +236,7 @@ MIX_MODES = context.JK.MIX_MODES .done((response) => @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) ) - .fail(() => + .fail((jqXHR) => @app.notifyServerError jqXHR, 'Search Unavailable' @setState({searching: false, first_search: false}) ) @@ -300,6 +323,7 @@ MIX_MODES = context.JK.MIX_MODES e.preventDefault() $target = $(e.target) params = id: $target.attr('data-jamtrack-id') + params.variant = $target.attr('data-variant') isFree = $(e.target).is('.is_free') @rest.addJamtrackToShoppingCart(params).done((response) => @@ -322,6 +346,18 @@ MIX_MODES = context.JK.MIX_MODES $parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack $parent.find('.license-us-why').on 'click', @licenseUSWhy $parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded + $parent.find('.jamtrack-variant-help').on 'click', @showVariantHelp + $parent.find('.jamtrack-upgrade-help').on 'click', @showUpgradeHelp + + showVariantHelp: (e) -> + $screen = $('#jamtrackFilter') + e.preventDefault() + context.JK.HelpBubbleHelper.jamtrackVariants($(e.target), $screen) + + showUpgradeHelp: (e) -> + $screen = $('#jamtrackFilter') + e.preventDefault() + context.JK.HelpBubbleHelper.jamtrackUpgrade($(e.target), $screen) toggleExpanded:(e) -> e.preventDefault() diff --git a/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee index 56adbf95c..4653ed190 100644 --- a/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee @@ -95,13 +95,37 @@ MIX_MODES = context.JK.MIX_MODES actionBtn = null if jamtrack.purchased - actionBtn = `PURCHASED` + if jamtrack.can_download + actionBtn = `PURCHASED` + else + priceNotice = `
$ {Number(jamtrack.upgrade_price).toFixed(2)}
` + actionBtn = `
+
+ {priceNotice} + UPGRADE TO FULL +
+ HELP +
` + + else if jamtrack.is_free - actionBtn = `GET IT FREE!` + actionBtn = `GET IT FREE!` else if jamtrack.added_cart actionBtn = `ALREADY IN CART` else - actionBtn = `ADD TO CART` + priceNotice = `
$ {jamtrack.price}
` + fullPriceNotice = `
$ {jamtrack.download_price}
` + actionBtn = `
+
+ {priceNotice} + ADD TO CART +
+
+ {fullPriceNotice} + ADD TO CART (FULL) +
+ HELP +
` availabilityNotice = null if jamtrack.sales_region==context.JK.AVAILABILITY_US @@ -136,7 +160,6 @@ MIX_MODES = context.JK.MIX_MODES
-
$ {jamtrack.price}
{actionBtn} {availabilityNotice}
@@ -326,12 +349,12 @@ MIX_MODES = context.JK.MIX_MODES .done((response) => @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) ) - .fail(() => + .fail((jqXHR) => @app.notifyServerError jqXHR, 'Search Unavailable' @setState({searching: false, first_search: false}) ) ) - .fail(() => + .fail((jqXHR) => @app.notifyServerError jqXHR, 'Search Unavailable' @setState({searching: false, first_search: false}) ) @@ -440,6 +463,7 @@ MIX_MODES = context.JK.MIX_MODES e.preventDefault() $target = $(e.target) params = id: $target.attr('data-jamtrack-id') + params.variant = $target.attr('data-variant') isFree = $(e.target).is('.is_free') @rest.addJamtrackToShoppingCart(params).done((response) => @@ -475,6 +499,18 @@ MIX_MODES = context.JK.MIX_MODES $parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack $parent.find('.license-us-why').on 'click', @licenseUSWhy $parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded + $parent.find('.jamtrack-variant-help').on 'click', @showVariantHelp + $parent.find('.jamtrack-upgrade-help').on 'click', @showUpgradeHelp + + showVariantHelp: (e) -> + $screen = $('#jamtrackSearch') + e.preventDefault() + context.JK.HelpBubbleHelper.jamtrackVariants($(e.target), $screen) + + showUpgradeHelp: (e) -> + $screen = $('#jamtrackSearch') + e.preventDefault() + context.JK.HelpBubbleHelper.jamtrackUpgrade($(e.target), $screen) toggleExpanded:(e) -> e.preventDefault() diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee index f687a330d..f8ba02b5d 100644 --- a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -502,7 +502,7 @@ UserStore = context.UserStore else if this.state.user.lesson_package_type_id == 'test-drive-2' explanation =`You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.` else - alert("You do not have a test drive package selected") + alert("You do not have a test drive package selected: " + this.state.user.lesson_package_type_id ) bookingDetail = `

{explanation} diff --git a/web/app/assets/javascripts/react-components/PayPalConfirmationScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/PayPalConfirmationScreen.js.jsx.coffee new file mode 100644 index 000000000..771716b92 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PayPalConfirmationScreen.js.jsx.coffee @@ -0,0 +1,160 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@PayPalConfirmationScreen = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")] + + render: () -> + content = null + + + if this.state.sold + + if context.jamClient && context.jamClient.IsNativeClient() + platformMessage = `

+

To play your purchased JamTrack, start a session and then open the JamTrack

+ +
+ Click Here to Start a Session +
+
+
` + else + platformMessage = + `` + content = `
+

Thank you for your order!

+ {platformMessage} +
` + else + orderButtons = {"button-orange": true, "place-order-btn": true, disabled: this.state.ordering } + cancelButtons = {"button-grey": true, "cancel": true, disabled: this.state.ordering } + + content = `
+

Confirm PayPal Payment

+ +

You have not yet made a payment via PayPal. Please review your purchase and confirm or cancel.

+ +
+ CONFIRM PURCHASE WITH + PAYPAL + CANCEL + +
+
+ + + ` + + `
+
+ {content} +
+
` + + placeOrder: (e) -> + e.preventDefault() + if this.state.ordering + return + @setState({ordering: true}) + + console.log("placing order with paypal") + @rest.paypalPlaceOrder() + .done((response) => + console.log("paypal detail obtained", response) + + @setState({sold: true, ordering: false}) + context.JK.JamTrackUtils.checkShoppingCart() + @app.refreshUser() + ) + .fail((jqXHR) => + @setState({ordering: false}) + if jqXHR.status == 404 + context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. Click Here to Checkout Again.') + else if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message) + else + context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com') + ) + + cancelOrder: (e) -> + e.preventDefault() + + window.location = '/client#/jamtrack' + + getInitialState: () -> + {} + + componentDidMount: () -> + + componentDidUpdate: () -> + + afterShow: (data) -> + rest.getShoppingCarts() + .done((carts) => + @setState({carts: carts}) + if carts.length == 0 + window.location = '/client#/jamtrack' + return + + @rest.paypalDetail() + .done((response) => + console.log("paypal detail obtained", response) + ) + .fail((jqXHR) => + if jqXHR.status == 404 + context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. Click Here to Checkout Again.') + else if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message) + else + context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com') + @app.notifyServerError jqXHR, 'PayPal Communication Error' + ) + ) + .fail((jqXHR) => + @app.notifyServerError jqXHR, 'Unable to fetch carts' + ) + + + beforeShow: () -> + this.setState({sold: false}) + + onAppInit: (@app) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + + screenBindings = + 'beforeShow': @beforeShow + 'afterShow': @afterShow + + @app.bindScreen('paypal/confirm', screenBindings) + + onUserChanged: (userState) -> + @user = userState?.user + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee index d0e812940..245ee6c15 100644 --- a/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee @@ -69,7 +69,7 @@ JamTrackPlayerStore = reactContext.JamTrackPlayerStore new window.Fingerprint2().get((result, components) => ( redirectTo = "/api/mixdowns/#{@state.mixdown.id}/download.mp3?file_type=mp3&sample_rate=#{@sampleRate}&download=1&mark=#{result}" - redirectTo = URI.escape(redirectTo) + redirectTo = encodeURIComponent(redirectTo) AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") )) diff --git a/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee index ea01460f5..2a5411a8f 100644 --- a/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee @@ -6,7 +6,6 @@ rest = context.JK.Rest() mixins = [] - # make sure this is actually us opening the window, not someone else (by checking for MixerStore) # this check ensures we attempt to listen if this component is created in a popup reactContext = if window.opener? then window.opener else window @@ -38,7 +37,8 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) weight = switch when jam_track_track.track_type == 'Master' then 0 when jam_track_track.track_type == 'Click' then 10000 - else jam_track_track.position + else + jam_track_track.position onJamTrackPlayerStoreChanged: (changes) -> #logger.debug("PopupMediaControls: jamtrack changed", changes) @@ -69,7 +69,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) AppActions.openExternalUrl($(e.target).attr('href')) render: () -> - closeLinkText = null header = null extraControls = null @@ -90,17 +89,17 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if selectedMixdown.client_state? switch selectedMixdown.client_state when 'download_fail' - customMixName = `
{selectedMixdown.name}
` + customMixName = `
{selectedMixdown.name}
` when 'downloading' - customMixName = `
Loading selected mix...
` + customMixName = `
Loading selected mix...
` when 'ready' customMixName = `
{selectedMixdown.name}
` disabled = false else if selectedMixdown.myPackage - customMixName = `
Creating mixdown...
` + customMixName = `
Creating mixdown...
` else - customMixName = `
{selectedMixdown.name}
` + customMixName = `
{selectedMixdown.name}
` else if selectedStem? if selectedStem.instrument @@ -117,20 +116,20 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if selectedStem.client_state? switch selectedStem.client_state when 'downloading' - customMixName = `
Loading {trackName}...
` + customMixName = `
Loading {trackName}...
` when 'download_fail' - customMixName = `
{trackName}
` + customMixName = `
{trackName}
` when 'ready' customMixName = `
{trackName}
` disabled = false else - customMixName = `
{trackName}
` + customMixName = `
{trackName}
` else if jamTrack?.client_state == 'downloading' - downloader = `` + downloader = `` else if jamTrack?.client_state == 'download_fail' - downloader = `` + downloader = `` jamTrackTypeHeader = `Full JamTrack {downloader}` @@ -153,15 +152,19 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) active = jamTrack.last_mixdown_id == null && jamTrack.last_stem_id == null myMixdowns.push ` -
+
Full JamTrack
- - - + + +
` @@ -186,26 +189,33 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if mixdown_package switch mixdown_package.signing_state when 'QUIET_TIMEOUT' - action = `` + action = `` when 'QUIET' action = `` when 'QUEUED' action = `` when 'QUEUED_TIMEOUT' - action = `` + action = `` when 'SIGNING' action = `` when 'SIGNING_TIMEOUT' - action = `` + action = `` when 'SIGNED' - action = `` + action = `` when 'ERROR' - action = `` + action = `` else - action = `` + action = `` if editing - mixdownName = `` + mixdownName = `` editIcon = `` else mixdownName = mixdown.name @@ -213,9 +223,11 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) # create hidden objects to deal with alginment issues using a table if !editIcon - editIcon = `` + editIcon = `` - download = `` + download = `` myMixdowns.push `
@@ -227,7 +239,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) {download} {editIcon} - +
` @@ -260,16 +272,21 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) myMixdowns.push `
- {trackOptions}
- - - + + +
` @@ -300,23 +317,29 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) tracks.push(` - {instrumentDescription} {part} - + {instrumentDescription} {part} + `) if jamTrack?.jmep?.Events? && jamTrack.jmep.Events[0].metronome? # tap-in detected; show user tap-in option tracks.push(` - Count-in - + Count-in + `) stems = `
- - + + + + + + {tracks} @@ -328,13 +351,15 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) nameClassData = {field: true} if @state.createMixdownErrors? - errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'}) + errorHtml = context.JK.reactErrors(@state.createMixdownErrors, + {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'}) - createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown}) + createMixClasses = classNames({'button-orange': true, 'create-mix-btn': true, 'disabled': @state.creatingMixdown}) mixControls = `
-

Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.

+

Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the + JamTrack. Then give your custom mix a name, and click the Create Mix button.

{stems}
@@ -410,14 +435,22 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if @state.showMyMixes - showMyMixesText = `hide my mixes
` + showMyMixesText = `hide my mixes +
+ ` else - showMyMixesText = `show my mixes
` + showMyMixesText = `show my mixes +
+ ` if @state.showCustomMixes - showMixControlsText = `hide mix controls
` + showMixControlsText = `hide mix controls +
+ ` else - showMixControlsText = `show mix controls
` + showMixControlsText = `show mix controls +
+ ` extraControls = `
@@ -432,20 +465,65 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
` - if helpLink? helpButton = `HELP` - `
- {header} - - {extraControls} -
- {helpButton} + `
+
+
+ jamtracks web player +
+
+ {header} + + {extraControls} +
+ {helpButton} +
+
+
+
+
+ helpful resources +
+
+ read a web player help article
+ that explains how to use all the features of this web player +
+
+ go to JamTracks homepage
+ where you can access all of your JamTracks, search for more JamTracks, and more +
+
+ download our free iOS app
+ that you can use to play with your JamTracks on your iPhone, iPad, or iPod Touch +
+
+ download our free Mac or Windows app
+ that gives you more powerful features to do more amazing things with your JamTracks +
+
+ review a list of help articles on the Mac/Win app
+ to understand all the things you can do with our free desktop app +
+
+ see more JamTracks by this artist
+ to check out other songs you might like +
+
+ download our complete JamTracks catalog
+ to browse or search through all the music we have in a convenient PDF file +
` - # {closeLinkText} + seeAllByArtist: (e) -> + e.preventDefault() + jamTrack = @state.jamTrackState?.jamTrack + + console.log("seeAllByArtist context", jamTrack) + window.open('/client?artist=' + encodeURIComponent(jamTrack.original_artist) + '#/jamtrack/search') + windowUnloaded: () -> JamTrackPlayerActions.windowUnloaded() unless window.DontAutoCloseMedia @@ -464,14 +542,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) e.preventDefault() verificationCheck: () -> - if @state.user?.email_needs_verification alert("Check your email and click the verification link. Refresh this page when done, and try again.") return @state.user?.email_needs_verification downloadMixdownReady: (mixdown, e) -> - if @verificationCheck() e.preventDefault() return @@ -494,7 +570,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}" activateStem: (e) -> - e.preventDefault() return if @verificationCheck() @@ -506,7 +581,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if !selectedTrackId? || selectedTrackId == '' alert("You must pick a track from the dropdown in order to play it.") else - @setState({editingMixdownId: null}) e.preventDefault() @@ -518,8 +592,38 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) # make this package the active one JamTrackPlayerActions.openStem(selectedTrackId) - downloadStem: (e) -> + addUpgradeToCart: (jamtrack) -> + console.log("adding upgrade to cart") + params = {id: jamtrack.id, variant: 'download'} + + rest.addJamtrackToShoppingCart(params).done((response) => + console.log("added item to shopping cart. fast_redeem? " + response.fast_redeem) + if response.fast_reedem + if context.JK.currentUserId? + window.open('/client#/redeemComplete') + else + window.open('/client#/redeemSignup') + else + window.open('/client#/shoppingCart') + + ).fail(((jqxhr) => + + handled = false + if jqxhr.status == 422 + body = JSON.parse(jqxhr.responseText) + if body.errors?.cart_id?[0] == 'has already been taken' + console.log("already taken, just show shopping cart") + window.open('/client#/shoppingCart') + return + else if body.errors && body.errors.base + handled = true + alert("You can not have a mix of free and non-free items in your shopping cart.\n\nIf you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.") + if !handled + alert("Error adding to shoppig cart. " + jqxhr.responseText) + )) + + downloadStem: (e) -> if @verificationCheck() e.preventDefault() return @@ -528,6 +632,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) selectedTrackId = $select.val() + if !@state.jamTrackState.jamTrack.can_download + e.preventDefault() + if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?") + @addUpgradeToCart(@state.jamTrackState.jamTrack) + return + if !selectedTrackId? || selectedTrackId == '' e.preventDefault() alert("You must select a track in order to download and also click the folder icon.") @@ -581,11 +691,17 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) JamTrackPlayerActions.activateNoMixdown(jamtrack) jamTrackDownload: (jamTrack, e) -> - if @verificationCheck() e.preventDefault() return + if !@state.jamTrackState.jamTrack.can_download + e.preventDefault() + if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?") + @addUpgradeToCart(@state.jamTrackState.jamTrack) + + return + if /iPhone|iPad|iPod/i.test(navigator.userAgent) # fall through @@ -622,14 +738,13 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) mixdownDelete: (mixdown) -> if @state.editingMixdownId? - @setState({editingMixdownId:null}) + @setState({editingMixdownId: null}) return if confirm("Delete this custom mix?") JamTrackPlayerActions.deleteMixdown(mixdown) mixdownError: (mixdown) -> - myPackage = mixdown.myPackage if myPackage? @@ -709,7 +824,11 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) tracks.push({id: stemId, mute: muted}) ) - mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch, "count-in": count_in, tracks:tracks}} + mixdown = { + jamTrackID: @state.jamTrackState.jamTrack.id, + name: name, + settings: {speed: speed, pitch: pitch, "count-in": count_in, tracks: tracks} + } JamTrackPlayerActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail) @@ -743,7 +862,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) fetchUserInfo: () -> rest.getUserDetail() .done((response) => - rest.postUserEvent({name: 'jamtrack_web_player_open'}) context.stats.write('web.jamtrack_web_player.open', { value: 1, @@ -757,13 +875,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) rest.userOpenedJamTrackWebPlayer() $root = $(@getDOMNode()) #context.JK.prodBubble($root.find('.create-mix-btn'), 'first-time-jamtrack-web-player', {}, {positions:['left'], offsetParent: $root}) - ), 1500) + ), 1500) ) componentDidMount: () -> - $(window).unload(@windowUnloaded) @root = jQuery(this.getDOMNode()) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 3f4dc1230..4f845d8df 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -1,7 +1,7 @@ context = window logger = context.JK.logger ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; - +rest = context.JK.Rest() mixins = [] @@ -523,12 +523,49 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) return if @verificationCheck() + if !@state.jamTrackState.jamTrack.can_download + e.preventDefault() + if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?") + @addUpgradeToCart(@state.jamTrackState.jamTrack) + return + new window.Fingerprint2().get((result, components) => ( redirectTo = "/api/jamtracks/#{jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1&mark=#{result}" - redirectTo = URI.escape(redirectTo) + redirectTo = encodeURIComponent(redirectTo) AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") )) + addUpgradeToCart: (jamtrack) -> + console.log("adding upgrade to cart") + + params = {id: jamtrack.id, variant: 'download'} + + rest.addJamtrackToShoppingCart(params).done((response) => + console.log("added item to shopping cart. fast_redeem? " + response.fast_redeem) + if response.fast_reedem + if context.JK.currentUserId? + AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/redeemComplete') + else + AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/redeemSignup') + else + AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/shoppingCart') + + ).fail(((jqxhr) => + + handled = false + if jqxhr.status == 422 + body = JSON.parse(jqxhr.responseText) + if body.errors?.cart_id?[0] == 'has already been taken' + console.log("already taken, just show shopping cart") + AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/shoppingCart') + return + else if body.errors && body.errors.base + handled = true + alert("You can not have a mix of free and non-free items in your shopping cart.\n\nIf you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.") + if !handled + alert("Error adding to shoppig cart. " + jqxhr.responseText) + )) + stemChanged:() -> stemDownload: (e) -> e.preventDefault() @@ -537,6 +574,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) $select = $(this.getDOMNode()).find('.active-stem-select') + if !@state.jamTrackState.jamTrack.can_download + e.preventDefault() + if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?") + @addUpgradeToCart(@state.jamTrackState.jamTrack) + return + selectedTrackId = $select.val() if !selectedTrackId? || selectedTrackId == '' @@ -546,7 +589,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) new window.Fingerprint2().get((result, components) => ( redirectTo = "/api/jamtracks/#{@state.jamTrackState.jamTrack.id}/stems/#{selectedTrackId}/download.mp3?file_type=mp3&download=1&mark=#{result}" - redirectTo = URI.escape(redirectTo) + redirectTo = encodeURIComponent(redirectTo) AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") )) @@ -591,7 +634,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) if browserPackage?.signing_state == 'SIGNED' new window.Fingerprint2().get((result, components) => ( redirectTo = "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}" - redirectTo = URI.escape(redirectTo) + redirectTo = encodeURIComponent(redirectTo) AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") )) else @@ -716,7 +759,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) setTimeout(@resizeWindow, 1000) shouldComponentUpdate: () -> - console.log("THIS UNLOADED", @unloaded) return !@unloaded resizeWindow: () => diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee index 8c9092b1e..d9c0ad6ce 100644 --- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -36,6 +36,10 @@ if accessOpener # this.setState(chatMixer: mixers.chatMixer) onRecordingStateChanged: (recordingState) -> + if @unloaded + #console.log("PopupMediaControls unloaded. ignore onMixersChnaged") + return + this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording) startStopRecording: () -> @@ -165,6 +169,9 @@ if accessOpener
` windowUnloaded: () -> + @unloaded = true + window.unloaded = true + window.opener.RecordingActions.recordingControlsClosed() onChatHelp: (e) -> @@ -215,6 +222,9 @@ if accessOpener $root = jQuery(this.getDOMNode()) $includeChat = $root.find('#include-chat') + shouldComponentUpdate: () -> + return !@unloaded + resizeWindow: () => $container = $('#minimal-container') width = $container.width() diff --git a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee index fb025d2e5..a631b41db 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee @@ -119,7 +119,7 @@ ConfigureTracksActions = @ConfigureTracksActions $connectionState, 'SessionStatsHover', () => - {participant: {client_id: this.props.clientId, user: name: 'You', possessive: 'Your'}, } + {myTrack: true, participant: {client_id: this.props.connStatsClientId, user: name: 'You', possessive: 'Your'}, } , {width:385, positions:['right', 'left'], offsetParent:$root.closest('.screen'), extraClasses: 'self'}) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee index 298dff75a..5566c8595 100644 --- a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee @@ -9,15 +9,29 @@ MixerActions = context.MixerActions session = sessionMixers.session mixers = sessionMixers.mixers noAudioUsers = mixers.noAudioUsers + clientsWithAudioOverride = mixers.clientsWithAudioOverride participants = [] if session.inSession() + self = session.getParticipant(@app.clientId) + + myJamBlasterClientId = null + if self? && self.client_role == 'child' && self.parent_client_id? + myJamBlasterClientId = self.parent_client_id + for participant in session.otherParticipants() - #if participant.is_jamblaster - #continue + if myJamBlasterClientId? && participant.client_id == myJamBlasterClientId + # don't show my parent jamblaster in 'others' + continue + + if participant.client_role == 'child' && participant.parent_client_id? + #participant.parent = session.getParticipant(participant.parent_client_id) + # don't show children nodes + continue + if participant.client_id == @app.clientId participant.user.possessive = "Your" @@ -55,6 +69,10 @@ MixerActions = context.MixerActions name = "#{name}: #{instrumentDescription}" + noAudio = false + if !clientsWithAudioOverride[participant.client_id] + noAudio = noAudioUsers[participant.client_id] + participantState = { participant: participant, tracks: tracks, @@ -63,7 +81,7 @@ MixerActions = context.MixerActions instrumentIcon: instrumentIcon, photoUrl: photoUrl, hasMixer: hasMixer, - noAudio: noAudioUsers[participant.client_id] + noAudio: noAudio } MixerActions.missingPeerMixer(participant.client_id) unless hasMixer diff --git a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee index 68ddde797..f2e91c536 100644 --- a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee @@ -81,7 +81,7 @@ MixerActions = @MixerActions
Volume
{monitorVolumeLeft}dB
- +
@@ -108,7 +108,7 @@ MixerActions = @MixerActions
Volume
{chatVolumeLeft}dB
- +
diff --git a/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee index 3eeac6bc1..f0c331233 100644 --- a/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee @@ -143,7 +143,7 @@ StatsInfo = { audio = @state.stats?.audio aggregateTag = null - if aggregate? + if aggregate? && !this.props.myTrack if aggregate.latency aggregateStats.push(@stat(aggregate, 'aggregate', 'Tot Latency', 'latency', Math.round(aggregate.latency))) @@ -173,10 +173,16 @@ StatsInfo = { audio_type = 'WDM' else if audio_long.indexOf('core') > -1 audio_type = 'CoreAudio' + else if audio_long.indexOf('alsa') > -1 + audio_type = 'JamBlaster' audioStats.push(@stat(audio, 'audio', 'Gear Driver', 'audio_in_type', audio_type)) if audio.framesize? framesize = '?' - if audio.framesize == 2.5 + if audio.framesize == 1.0 + framesize = '1 ms' + else if audio.framesize == 2.0 + framesize = '1 ms' + else if audio.framesize == 2.5 framesize = '2.5 ms' else if audio.framesize == 5 framesize = '5 ms' @@ -185,7 +191,7 @@ StatsInfo = { audioStats.push(@stat(audio, 'audio', 'Frame Size', 'framesize', framesize)) networkTag = null - if network? + if network? && !this.props.myTrack if network.ping? networkStats.push(@stat(network, 'network', 'Latency', 'ping', (network.ping / 2).toFixed(1) + ' ms')) if network.audiojq_median? @@ -238,7 +244,11 @@ StatsInfo = { onStatsChanged: (stats) -> stats = window.SessionStatsStore.stats if stats? - clientStats = stats[@props.participant.client_id] + if stats.parent? + # if we have a parent, then use stats from the JamBlaster (parent), not ourselves. Otherwise we'll get bad stats (no Audio etc) + clientStats = stats.parent[@props.participant.client_id] + else + clientStats = stats[@props.participant.client_id] else clientStats = null @setState({stats: clientStats}) diff --git a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee index 000d176c7..aa2781b65 100644 --- a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee @@ -8,6 +8,7 @@ MIX_MODES = context.JK.MIX_MODES propTypes: { gainType: React.PropTypes.string + controlGroup: React.PropTypes.string } getInitialState: () -> @@ -22,12 +23,11 @@ MIX_MODES = context.JK.MIX_MODES mixers = @state.mixers.mixer - # if this is a media track, jam track , or media category, affect volume of both mixer and opposing mixer if @state.mixers.mixer.group_id == ChannelGroupIds.MediaTrackGroup || @state.mixers.mixer.group_id == ChannelGroupIds.JamTrackGroup || ((@state.mixers.mixer.group_id == ChannelGroupIds.MonitorCatGroup || @state.mixers.mixer.group_id == ChannelGroupIds.MasterCatGroup) && @state.mixers.mixer.name == CategoryGroupIds.MediaTrack) - MixerActions.faderChanged(data, [@state.mixers.mixer, @state.mixers.oppositeMixer], @props.gainType) + MixerActions.faderChanged(data, [@state.mixers.mixer, @state.mixers.oppositeMixer], @props.gainType, @props.controlGroup) else - MixerActions.faderChanged(data, mixers, @props.gainType) + MixerActions.faderChanged(data, mixers, @props.gainType, @props.controlGroup) render: () -> # mixer can be a single item or array diff --git a/web/app/assets/javascripts/react-components/ShoppingCartContents.js.jsx.coffee b/web/app/assets/javascripts/react-components/ShoppingCartContents.js.jsx.coffee new file mode 100644 index 000000000..3d8d70ada --- /dev/null +++ b/web/app/assets/javascripts/react-components/ShoppingCartContents.js.jsx.coffee @@ -0,0 +1,106 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@ShoppingCartContents = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")] + + render: () -> + + carts = [] + + if this.props.carts? + if this.props.carts.length == 0 + carts = `
You have nothing in your cart
` + else + taxRate = 0 + if this.props.tax + taxRate = 0.0825 + + estimatedTax = 0 + estimatedTotal = 0 + + for cart in this.props.carts + cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem + estimatedTax += cart.product_info.price * cart_quantity * taxRate + estimatedTotal += cart.product_info.price * cart_quantity + + estimatedTax = Math.round(estimatedTax * 100) / 100 + estimatedTotal = Math.round((estimatedTotal + estimatedTax) * 100) / 100 + + for cart in this.props.carts + console.log("CART", cart) + freeNotice = null + if cart.product_info.free + freeNotice = `| (first one free)` + carts.push(`
+
+ {cart.product_info.sale_display} + {freeNotice} +
+
+ $ {Number(cart.product_info.real_price).toFixed(2)} +
+
+ {cart.quantity} +
+
+
`) + + carts.push(`
+
+ Tax +
+
+ $ {estimatedTax.toFixed(2)} +
+
+ +
+
+
`) + + carts.push(`
+
+ Total +
+
+ $ {estimatedTotal.toFixed(2)} +
+
+ +
+
+
`) + else + carts = `
Loading...
` + + `
+
+
+
+ YOUR ORDER INCLUDES: +
+
+ PRICE +
+
+ QUANTITY +
+
+ {carts} +
+
+
+
` + + onAppInit: (@app) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + + + onUserChanged: (userState) -> + @user = userState?.user + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee index aaefaf606..43d0a0902 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -43,6 +43,7 @@ proficiencyDescriptionMap = { TILE_RATINGS: 'ratings' TILE_PRICES: 'prices' visible: false + profileClipboard: null TILES: ['about', 'experience', 'samples', 'ratings', 'prices'] @@ -65,11 +66,26 @@ proficiencyDescriptionMap = { @root = $(@getDOMNode()) @screen = $('#teacher-profile') @starbox() + @clipboard() componentDidUpdate:() -> @starbox() context.JK.popExternalLinks(@root) + @clipboard() + + clipboard: () -> + $profileLink = @root.find('.copy-profile-link') + + if $profileLink.length > 0 && !@profileClipboard? + # mount it + @profileClipboard = new Clipboard($profileLink.get(0), { + text: => + return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.teacher?.id) + }) + else if $profileLink.length == 0 && @profileClipboard? + @profileClipboard.destroy() + @profileClipboard = null starbox:() -> $ratings = @root.find('.ratings-box') @@ -230,7 +246,9 @@ proficiencyDescriptionMap = { biography = biography.replace(/\n/g, "
") `
-

Teacher Profile {this.editProfileLink('edit profile', 'introduction')}

+ + COPY PROFILE URL TO CLIPBOARD +

Teacher Profile {this.editProfileLink('edit profile', 'introduction')}

@@ -694,6 +712,16 @@ proficiencyDescriptionMap = {
` + copyProfileLink: (e) -> + + e.preventDefault() + + @app.layout.notify({ + title: 'Teacher Profile Link Copied', + text: "Your clipboard now has a link to this teacher that you can share with anyone." + }) + + selectionMade: (selection, e) -> e.preventDefault() diff --git a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee index 68c71dfba..cbb7916cc 100644 --- a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee @@ -14,4 +14,5 @@ context = window metronomeChanged: {} deadUserRemove: {} missingPeerMixer: {} + clientsWithAudio: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee index f6b111ac9..ad49fc0ca 100644 --- a/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee @@ -11,4 +11,5 @@ context = window abortedRecording: {} openRecordingControls: {} recordingControlsClosed: {} + mixTransferred: {} }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index aa28215d6..a475caad5 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -8,7 +8,7 @@ MIX_MODES = context.JK.MIX_MODES; @MixerHelper = class MixerHelper - constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) -> + constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixMode) -> @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions @app = @session.app @mixersByResourceId = {} @@ -622,13 +622,12 @@ MIX_MODES = context.JK.MIX_MODES; return mixerPair.personal - findMixerForTrack: (client_id, track, myTrack, mode = MIX_MODES.PERSONAL) -> + findMixerForTrack: (client_id, track, myTrack, mode) -> mixer = null # what is the best mixer for this track/client ID? oppositeMixer = null # what is the corresponding mixer in the opposite mode? vuMixer = null muteMixer = null - if myTrack # when it's your track, look it up by the backend resource ID mixer = @getMixerByTrackId(track.client_track_id, mode) @@ -674,7 +673,7 @@ MIX_MODES = context.JK.MIX_MODES; oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] if !oppositeMixer - logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers ) + logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers) when MIX_MODES.PERSONAL mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) @@ -693,6 +692,8 @@ MIX_MODES = context.JK.MIX_MODES; logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer) #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs + else + logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers) { mixer: mixer, @@ -729,26 +730,43 @@ MIX_MODES = context.JK.MIX_MODES; originalVolume - faderChanged: (data, mixers, gainType) -> + faderChanged: (data, mixers, gainType, controlGroup) -> mixers = [mixers] unless $.isArray(mixers) originalVolume = @getOriginalVolume(mixers, gainType) - for mixer in mixers - broadcast = !(data.dragging) # If fader is still dragging, don't broadcast - mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + if controlGroup? + mixers = [mixers[0]] - relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack) + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) - @setMixerVolume(mixer, data.percentage, relative, originalVolume) + relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack) - # keep state of mixer in sync with backend - mixer = @getMixer(mixer.id, mixer.mode) - mixer.volume_left = context.trackVolumeObject.volL + @setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup) - #if groupId == ChannelGroupIds.UserMusicInputGroup - # # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well - # context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.volume_left = context.trackVolumeObject.volL + + else + + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + + relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack) + + @setMixerVolume(mixer, data.percentage, relative, originalVolume) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.volume_left = context.trackVolumeObject.volL + + #if groupId == ChannelGroupIds.UserMusicInputGroup + # # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well + # context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) initGain: (mixer) -> if $.isArray(mixer) @@ -791,7 +809,7 @@ MIX_MODES = context.JK.MIX_MODES; mixer = @getMixer(mixer.id, mixer.mode) mixer.loop = context.trackVolumeObject.loop - setMixerVolume: (mixer, volumePercent, relative, originalVolume) -> + setMixerVolume: (mixer, volumePercent, relative, originalVolume, controlGroup) -> ### // The context.trackVolumeObject has been filled with the mixer values // that go with mixerId, and the range of that mixer @@ -821,7 +839,15 @@ MIX_MODES = context.JK.MIX_MODES; else context.trackVolumeObject.volL = newVolume context.trackVolumeObject.volR = newVolume - context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + if controlGroup? + + if mixer.mode == MIX_MODES.PERSONAL + controlGroupsArg = 0 + else + controlGroupsArg = 1 + context.jamClient.setSessionMixerCategoryPlayoutState(controlGroup == 'music', controlGroupsArg); + else + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); percentFromMixerValue: (min, max, value) -> try diff --git a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee index 204353571..5ae864747 100644 --- a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee @@ -98,7 +98,7 @@ rest = context.JK.Rest()
  • And more…
  • - JamTracks sell for $1.99 each. Musicians love to play with these, and typically buy a few at a + JamTracks sell for $1.99 each ($4.99 to include ability to download). Musicians love to play with these, and typically buy a few at a time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee index 490751fd5..1d2cdea72 100644 --- a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee @@ -72,7 +72,7 @@ rest = context.JK.Rest() {img} - $1.99 value + #{this.props.jam_track.download_price} value


    diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee index b3a19c414..0ccbe995e 100644 --- a/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee @@ -35,10 +35,10 @@ rest = context.JK.Rest() if loggedIn loggedInCtaButton = `` if !@isFree() - loggedInPriceAdvisory = `
    ${this.props.jam_track.price}
    ` + loggedInPriceAdvisory = `
    ${this.props.jam_track.download_price}
    ` else if !@isFree() - loggedOutPriceAdvisory = `
    ${this.props.jam_track.price}
    ` + loggedOutPriceAdvisory = `
    ${this.props.jam_track.download_price}
    ` if this.state.loginErrors? for key, value of this.state.loginErrors diff --git a/web/app/assets/javascripts/react-components/landing/ProductJamBlasterBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterBottomPage.js.jsx.coffee index 44fae616f..a56e46588 100644 --- a/web/app/assets/javascripts/react-components/landing/ProductJamBlasterBottomPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterBottomPage.js.jsx.coffee @@ -108,7 +108,7 @@ context = window
    TRACKSmute
    TRACKSmute