diff --git a/admin/Gemfile b/admin/Gemfile index dd780afd7..a809dd0ee 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -73,7 +73,7 @@ gem 'iso-639' gem 'rubyzip' gem 'sanitize' gem 'slim' -gem 'influxdb' +#gem 'influxdb' gem 'cause' # needed by influxdb gem 'influxdb-rails', '0.1.10' gem 'recurly' @@ -107,7 +107,6 @@ end group :development, :test do gem 'capybara' gem 'rspec-rails', '2.14.2' - gem 'guard-rspec' gem 'jasmine', '1.3.1' gem 'execjs', '1.4.0' #gem 'therubyracer' #, '0.11.0beta8' diff --git a/admin/app/admin/interested_schools.rb b/admin/app/admin/interested_schools.rb new file mode 100644 index 000000000..fb3a1abe0 --- /dev/null +++ b/admin/app/admin/interested_schools.rb @@ -0,0 +1,20 @@ +ActiveAdmin.register JamRuby::User, :as => 'SchoolInterest' do + + menu :label => 'Interested in Schools', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.where(school_interest: true) } + + index do + column "Name" do |user| + span do + link_to "#{user.name} (#{user.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/students.rb b/admin/app/admin/students.rb index a022aa727..75c099455 100644 --- a/admin/app/admin/students.rb +++ b/admin/app/admin/students.rb @@ -6,12 +6,13 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do config.batch_actions = false config.per_page = 100 config.paginate = true + config.filters = false - def booked_anything(scope) + def booked_anything(scope) scope.joins(:student_lesson_bookings).where('lesson_bookings.active = true').uniq end - scope("Default", default: true) { |scope| booked_anything(scope).order('ready_for_session_at IS NULL DESC') } + scope("Default", default: true) { |scope| scope.where('is_a_student = true OR ((select count(id) from lesson_bookings where lesson_bookings.user_id = users.id) > 0)').order('users.ready_for_session_at IS NULL DESC') } index do column "Name" do |user| @@ -45,8 +46,8 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do end end column "School" do |user| - if teacher.school - teacher.school.name + if user.school + user.school.name end end end diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb index 942e501d5..e54718565 100644 --- a/admin/app/admin/teachers.rb +++ b/admin/app/admin/teachers.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do - menu :label => 'Teacher', :parent => 'JamClass' + menu :label => 'Teachers', :parent => 'JamClass' config.sort_order = 'created_at desc' config.batch_actions = false diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 62739af59..649f3aaaf 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -1,18 +1,21 @@ GEM remote: http://rubygems.org/ specs: - little-plugger (1.1.3) + little-plugger (1.1.4) logging (1.7.2) little-plugger (>= 1.1.3) pg (0.17.1) - pg_migrate (0.1.13) + pg_migrate (0.1.14) logging (= 1.7.2) pg (= 0.17.1) thor - thor (0.18.1) + thor (0.19.1) PLATFORMS ruby DEPENDENCIES - pg_migrate (= 0.1.13)! + pg_migrate (= 0.1.14)! + +BUNDLED WITH + 1.11.2 diff --git a/db/manifest b/db/manifest index 4067937f3..68db33231 100755 --- a/db/manifest +++ b/db/manifest @@ -341,4 +341,10 @@ email_blacklist.sql jamblaster_connection.sql teacher_progression.sql teacher_complete.sql -lessons.sql \ No newline at end of file +lessons.sql +lessons_unread_messages.sql +track_school_signups.sql +add_test_drive_types.sql +updated_subjects.sql +update_payment_history.sql +lesson_booking_schools.sql \ No newline at end of file diff --git a/db/up/add_test_drive_types.sql b/db/up/add_test_drive_types.sql new file mode 100644 index 000000000..01584db3d --- /dev/null +++ b/db/up/add_test_drive_types.sql @@ -0,0 +1,4 @@ +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-2', 'Test Drive (2)', 'Two reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-2', 29.99); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-1', 'Test Drive (1)', 'One reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-1', 15.99); +UPDATE lesson_package_types set name = 'Test Drive (4)', package_type = 'test-drive-4' WHERE id = 'test-drive'; +ALTER TABLE users ADD COLUMN lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id); \ No newline at end of file diff --git a/db/up/lesson_booking_schools.sql b/db/up/lesson_booking_schools.sql new file mode 100644 index 000000000..265e731b7 --- /dev/null +++ b/db/up/lesson_booking_schools.sql @@ -0,0 +1,3 @@ +ALTER TABLE lesson_bookings ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teacher_payments ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teacher_distributions ADD COLUMN school_id INTEGER REFERENCES schools(id); \ No newline at end of file diff --git a/db/up/lessons_unread_messages.sql b/db/up/lessons_unread_messages.sql new file mode 100644 index 000000000..1b2d65f26 --- /dev/null +++ b/db/up/lessons_unread_messages.sql @@ -0,0 +1,34 @@ +ALTER TABLE chat_messages DROP COLUMN lesson_booking_id; +ALTER TABLE chat_messages ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE chat_messages ADD COLUMN purpose VARCHAR(200); +ALTER TABLE lesson_sessions ADD COLUMN student_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN teacher_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN sent_starting_notice BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_counter_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_counter_slot_id_fkey FOREIGN KEY (counter_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_default_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_default_slot_id_fkey FOREIGN KEY (default_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + + +ALTER TABLE lesson_sessions DROP CONSTRAINT lesson_sessions_slot_id_fkey; +ALTER TABLE lesson_sessions ADD CONSTRAINT lesson_sessions_slot_id_fkey FOREIGN KEY (slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE users DROP CONSTRAINT users_teacher_id_fkey; +ALTER TABLE users ADD CONSTRAINT users_teacher_id_fkey FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE; + +ALTER TABLE music_sessions DROP CONSTRAINT music_sessions_lesson_session_id_fkey; +ALTER TABLE music_sessions ADD CONSTRAINT music_sessions_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + +ALTER TABLE notifications DROP CONSTRAINT notifications_lesson_session_id_fkey; +ALTER TABLE notifications ADD CONSTRAINT notifications_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_lesson_session_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_target_user_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_target_user_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + diff --git a/db/up/track_school_signups.sql b/db/up/track_school_signups.sql new file mode 100644 index 000000000..7e66f48fc --- /dev/null +++ b/db/up/track_school_signups.sql @@ -0,0 +1 @@ +ALTER TABLE USERS ADD COLUMN school_interest BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/update_payment_history.sql b/db/up/update_payment_history.sql new file mode 100644 index 000000000..0e3e345ca --- /dev/null +++ b/db/up/update_payment_history.sql @@ -0,0 +1 @@ +ALTER TABLE charges ADD COLUMN user_id VARCHAR(64) REFERENCES users(id); \ No newline at end of file diff --git a/db/up/updated_subjects.sql b/db/up/updated_subjects.sql new file mode 100644 index 000000000..4bd7873e1 --- /dev/null +++ b/db/up/updated_subjects.sql @@ -0,0 +1,27 @@ +-- https://jamkazam.atlassian.net/browse/VRFS-3407 +UPDATE subjects SET description = 'Composition' WHERE id = 'composing'; +UPDATE subjects SET description = 'Recording & Production' WHERE id = 'recording'; +UPDATE subjects SET description = 'Sight Reading' WHERE id = 'site-reading'; + +INSERT INTO subjects(id, description) VALUES ('film-scoring', 'Film Scoring'); +INSERT INTO subjects(id, description) VALUES ('video-game-scoring', 'Video Game Scoring'); +INSERT INTO subjects(id, description) VALUES ('ear-training', 'Ear Training'); +INSERT INTO subjects(id, description) VALUES ('harmony', 'Harmony'); +INSERT INTO subjects(id, description) VALUES ('music-therapy', 'Music Therapy'); +INSERT INTO subjects(id, description) VALUES ('songwriting', 'Songwriting'); +INSERT INTO subjects(id, description) VALUES ('conducting', 'Conducting'); +INSERT INTO subjects(id, description) VALUES ('instrument-repair', 'Instrument Repair'); +INSERT INTO subjects(id, description) VALUES ('improvisation', 'Improvisation'); +INSERT INTO subjects(id, description) VALUES ('pro-tools', 'Pro Tools'); +INSERT INTO subjects(id, description) VALUES ('ableton-live', 'Ableton Live'); +INSERT INTO subjects(id, description) VALUES ('fl-studio', 'FL Studio'); +INSERT INTO subjects(id, description) VALUES ('garageband', 'GarageBand'); +INSERT INTO subjects(id, description) VALUES ('apple-logic-pro', 'Apple Logic Pro'); +INSERT INTO subjects(id, description) VALUES ('presonus-studio-one', 'PreSonus Studio One'); +INSERT INTO subjects(id, description) VALUES ('reaper', 'Reaper'); +INSERT INTO subjects(id, description) VALUES ('cubase', 'Cubase'); +INSERT INTO subjects(id, description) VALUES ('sonar', 'Sonar'); +INSERT INTO subjects(id, description) VALUES ('reason', 'Reason'); +INSERT INTO subjects(id, description) VALUES ('amplitube', 'AmpliTube'); +INSERT INTO subjects(id, description) VALUES ('line-6-pod', 'Line 6 Pod'); +INSERT INTO subjects(id, description) VALUES ('guitar-ring', 'Guitar Rig'); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 57799fc7b..207d61e5d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -619,6 +619,8 @@ message ChatMessage { optional string msg_id = 4; optional string created_at = 5; optional string channel = 6; + optional string lesson_session_id = 7; + optional string purpose = 8; } message SendChatMessage { diff --git a/ruby/Gemfile b/ruby/Gemfile index 27fdc30a6..03fd91455 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -49,7 +49,7 @@ gem 'rest-client' gem 'iso-639' gem 'rubyzip' gem 'sanitize' -gem 'influxdb' +#gem 'influxdb' gem 'recurly' gem 'sendgrid_toolkit', '>= 1.1.1' gem 'stripe' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 2fddb85f5..e2e86525e 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -57,6 +57,7 @@ require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/hourly_job" +require "jam_ruby/resque/scheduled/minutely_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_reminder" diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb index f1c4e92f0..6f8db1c02 100644 --- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -36,6 +36,14 @@ module JamRuby subject: options[:subject]) end + def partner(options) + mail(to: APP_CONFIG.email_partners_alias, + from: APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + def recurly_alerts(user, options) body = options[:body] diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index db0933054..44410c3e8 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -81,6 +81,22 @@ module JamRuby end end + def school_owner_welcome_message(user) + @user = user + @subject= "Welcome to JamKazam and JamClass online lessons!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + def password_changed(user) @user = user @@ -912,7 +928,7 @@ module JamRuby @lesson_session = lesson_session email = @student.email - subject = "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits" + subject = "You have used #{@student.remaining_test_drives} of #{@student.total_test_drives} TestDrive lesson credits" unique_args = {:type => "student_test_drive_success"} sendgrid_category "Notification" @@ -927,6 +943,31 @@ module JamRuby end end + def student_test_drive_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your TestDrive with #{@teacher.name} will not be billed" + unique_args = {:type => "student_test_drive_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + # successfully completed, but no more test drives left def student_test_drive_lesson_done(lesson_session) @@ -939,7 +980,7 @@ module JamRuby @lesson_session = lesson_session email = @student.email - subject = "You have used all 4 TestDrive lesson credits" + subject = "You have used all TestDrive lesson credits" unique_args = {:type => "student_test_drive_success"} sendgrid_category "Notification" @@ -1229,12 +1270,39 @@ module JamRuby end end + # always goes to the teacher def teacher_distribution_done(teacher_payment) + @school = teacher_payment.school @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution @teacher = teacher_payment.teacher + @payable_teacher = teacher_payment.payable_teacher + @name = @teacher.first_name || 'Anonymous' + @student = @distribution.student email = @teacher.email - @subject = "You have received payment for your participation in JamClass" + if @school + if @distribution.is_test_drive? + @subject = "Your TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "Your lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "Your #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "Your lesson with #{@student.name}" + end + else + if @distribution.is_test_drive? + @subject = "You have earned #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}" + elsif @distribution.is_normal? + @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + elsif @distribution.is_monthly? + @subject = "You have earned #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}" + else + @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + end + end + unique_args = {:type => "teacher_distribution_done"} sendgrid_category "Notification" @@ -1249,24 +1317,51 @@ module JamRuby end end + # if school, goes to school owner; otherwise goes to teacher def teacher_distribution_fail(teacher_payment) + @school = teacher_payment.school @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution @teacher = teacher_payment.teacher - email = @teacher.email + @payable_teacher = teacher_payment.payable_teacher + @student = @distribution.student + @name = @payable_teacher.first_name || 'Anonymous' + email = @payable_teacher.email @card_declined = teacher_payment.is_card_declined? @card_expired = teacher_payment.is_card_expired? @bill_date = teacher_payment.last_billed_at_date - @subject = "We were unable to pay you today" + if @school + if @distribution.is_test_drive? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}" + end + else + if @distribution.is_test_drive? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}" + elsif @distribution.is_normal? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + elsif @distribution.is_monthly? + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}" + else + @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}" + end + end + unique_args = {:type => "teacher_distribution_fail"} sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [@teacher.id]) + sendgrid_substitute('@USERID', [@payable_teacher.id]) mail(:to => email, :subject => @subject) do |format| format.text @@ -1274,6 +1369,42 @@ module JamRuby end end + # always goes to the school owner + def school_distribution_done(teacher_payment) + @school = teacher_payment.school + @teacher_payment = teacher_payment + @distribution = teacher_payment.teacher_distribution + @teacher = teacher_payment.teacher + @payable_teacher = @school.owner + @name = @payable_teacher.first_name || 'Anonymous' + @student = @distribution.student + email = @payable_teacher.email + + if @distribution.is_test_drive? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for TestDrive lesson with #{@student.name}" + elsif @distribution.is_normal? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}" + elsif @distribution.is_monthly? + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for #{@distribution.month_name} lessons with #{@student.name}" + else + @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}" + end + + unique_args = {:type => "school_distribution_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@payable_teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def monthly_recurring_done(lesson_session) @student = lesson_session.student @teacher = lesson_session.teacher @@ -1485,5 +1616,78 @@ module JamRuby format.html end end + + def lesson_chat(chat_msg) + @target = chat_msg.target_user + @sender = chat_msg.user + @message = chat_msg.message + @lesson_session = chat_msg.lesson_session + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @target.email + @subject = "#{@sender.name} has sent you a message about a lesson" + unique_args = {:type => "lesson_chat"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@target.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def lesson_starting_soon_teacher(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @teacher.email + @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def lesson_starting_soon_student(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @student.email + @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_student"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb new file mode 100644 index 000000000..4b11f3cc0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

+ + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view the entire lesson conversation.

+

+ VIEW LESSON CONVERSATION +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb new file mode 100644 index 000000000..43b683250 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb @@ -0,0 +1,7 @@ +<%= @subject %> + +<%= @sender.name %> says: + +<%= @message %> + +To see the full lesson conversation, click here: <%= @lesson_session.chat_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb new file mode 100644 index 000000000..d4744ff94 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb new file mode 100644 index 000000000..f880fcb55 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb @@ -0,0 +1,4 @@ +Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. + +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb new file mode 100644 index 000000000..96ecf3c06 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb new file mode 100644 index 000000000..c8f460e25 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb @@ -0,0 +1,3 @@ +Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb index 96bf19889..bc205e230 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb @@ -7,7 +7,7 @@

- We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance. + We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>.

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb new file mode 100644 index 000000000..37f9d2780 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> + +

+ Hello <%= @name %>, +

+ + <% if @distribution.is_test_drive? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.

+ <% elsif @distribution.is_normal? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.

+ <% elsif @distribution.is_monthly? %> +

We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons.

+ <% else %> + Unknown payment type. + <% end %> +
+
+ +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb new file mode 100644 index 000000000..fcf56ab98 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @name %>, + +<% if @distribution.is_test_drive? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson. +<% elsif @distribution.is_normal? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson. +<% elsif @distribution.is_monthly? %> +We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons. +<% else %> +Unknown payment type. +<% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb new file mode 100644 index 000000000..7df3b4c61 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb @@ -0,0 +1,39 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +

+ Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business. +

+ +

+ We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner: +

+ + +

Guide for Music School Owners
+ These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on. +

+ +

Guide for Music Lesson Teachers
+ These help articles explain how teachers use the features of the platform outside of the online lesson sessions. +

+ +

Key Features To Use In Online Sessions
+ These help articles explain the key features instructors can use in online sessions to teach effectively. +

+ +

+ Gear Requirements
+ This help article explains the requirements for your computer, audio and video gear, and Internet service. +

+ +

+ Thanks again for connecting with us, and we look forward to speaking with you soon! +

+

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb new file mode 100644 index 000000000..771a82b03 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb @@ -0,0 +1,24 @@ +<% if !@user.anonymous? %> +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> +<% end %> + +Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business. + +We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner: + +-- Guide for Music School Owners (https://jamkazam.desk.com/customer/en/portal/topics/935633-jamclass-online-music-lessons---for-music-schools/articles) +These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on. + +-- Guide for Music Lesson Teachers (https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles) +These help articles explain how teachers use the features of the platform outside of the online lesson sessions. + +-- Key Features To Use In Online Sessions (https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles) +These help articles explain the key features instructors can use in online sessions to teach effectively. + +-- Gear Requirements (https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements) +This help article explains the requirements for your computer, audio and video gear, and Internet service. + +Thanks again for connecting with us, and we look forward to speaking with you soon! + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb index d60afb1dc..a5b2c826a 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb @@ -14,7 +14,7 @@
<%= @message %>
<% end %> -

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

VIEW LESSON DETAILS diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb index 1a3c7e42a..a6a2cddc7 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb @@ -7,8 +7,7 @@

- We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been - billed $<%= @lesson_session.amount_charged %> for today's lesson. + We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson.

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb index f7836f6ce..aa263319f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb @@ -6,7 +6,7 @@ Hello <%= @student.name %>,

-

You will not be billed for today's session with <%= @teacher.name %> +

You will not be billed for today's session with <%= @teacher.name %>.

Click the button below to see more information about this session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb index 0c6d60a65..31ef00ca0 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb @@ -1,5 +1,5 @@ Hello <%= @student.name %>, -You will not be billed for today's session with <%= @teacher.name %> +You will not be billed for today's session with <%= @teacher.name %>. To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb index 67590256e..f192b7b9c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb @@ -8,4 +8,4 @@ <%= @session_date %>

-

VIEW LESSON DETAILS

\ No newline at end of file +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb index c0e1bcb4d..40ff80156 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -1,4 +1,4 @@ -<% provide(:title, "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits") %> +<% provide(:title, "You have used #{@student.remaining_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %> <% provide(:photo_url, @teacher.resolved_photo_url) %> <% content_for :note do %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb index 37ea61020..3e3b6ea4f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -1,4 +1,4 @@ -You have used <%= @student.remaining_test_drives %> of 4 TestDrive lesson credits. +You have used <%= @student.remaining_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits. <% if @student.has_rated_teacher(@teacher) %> Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb index b5e14e612..d99b8494a 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb @@ -1,11 +1,11 @@ -<% provide(:title, "You have used all 4 TestDrive lesson credits") %> +<% provide(:title, "You have used all TestDrive lesson credits") %>

Hello <%= @student.name %>,

- We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have now used all 4 TestDrive credits. + We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have now used all your TestDrive credits. <% if !@student.has_rated_teacher(@teacher) %> Please rate your teacher now for today’s lesson to diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb index 2d4d02829..f80c5c8cb 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb @@ -1,4 +1,4 @@ -You have used all of your 4 TestDrive lesson credits. +You have used all of your TestDrive lesson credits. <% if @student.has_rated_teacher(@teacher) %> Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb new file mode 100644 index 000000000..deee9c563 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your TestDrive with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You have not used a credit for today's TestDrive with <%= @teacher.name %>. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb new file mode 100644 index 000000000..f3d3b2825 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You have not used a credit for today's TestDrive with <%= @teacher.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb index 88aacda6e..574aa3885 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -32,7 +32,7 @@ teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click with. It's a phenomenal value, and then you can stick with the best teacher for you. - Click this link + Click this link to sign up now for TestDrive. Then you can book 4 TestDrive lessons to get rolling.

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb index aa851747f..0cff2ed3e 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb @@ -18,7 +18,7 @@ TestDrive lets you take 4 full lessons (30 minutes each) from 4 different teache teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click with. It's a phenomenal value, and then you can stick with the best teacher for you. -Click this link to sign up now for TestDrive (https://www.jamkazam.com/client#/jamclass/book-lesson/purchase_test-drive). +Click this link to sign up now for TestDrive (https://www.jamkazam.com/client#/jamclass/test-drive-selection). Then you can book 4 TestDrive lessons to get rolling. 2. Set Up Your Gear diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb index af2681325..fc3c62e31 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb @@ -1,31 +1,42 @@ <% provide(:title, @subject) %> -

You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:

-
+

+ Hello <%= @name %>, +

-<% @teacher_payment.teacher_distributions.each do |distribution| %> - - <% if distribution.is_test_drive? %> -

You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>

+ <% if @distribution.is_test_drive? %> + <% if @school %> +

We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>.

+ <% else %> +

You have earned $<%= @distribution.real_distribution_display %> for your TestDrive lesson with <%= @distribution.student.name %>.

+ <% end %>

- <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> - If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <% end %> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.

- <% elsif distribution.is_normal? %> -

You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>

+ <% elsif @distribution.is_normal? %> + <% if @school %> +

we hope you enjoyed your lesson today with <%= @distribution.student.name %>.

+ <% else %> +

You have earned $<%= @distribution.real_distribution_display %> for your lesson with <%= @distribution.student.name %>.

+ <% end %>

- <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> - If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <% end %> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.

- <% elsif distribution.is_monthly? %> -

You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>

+ <% elsif @distribution.is_monthly? %> + <% if @school %> +

we hope you enjoyed your <%= @distribution.month_name %> lessons with <%= @distribution.student.name %>.

+ <% else %> +

You have earned $<%= @distribution.real_distribution_display %> for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>.

+ <% end %>

- <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> - If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <% end %> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.

@@ -35,7 +46,5 @@

-<% end %> - Best Regards,
JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb index 2e18fa163..17ecc5c92 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb @@ -1,31 +1,40 @@ <% provide(:title, @subject) %> -You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details: +Hello <%= @name %>, -<% @teacher_payment.teacher_distributions.each do |distribution| %> - -<% if distribution.is_test_drive? %> -You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>. -<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> -If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% if @distribution.is_test_drive? %> +<% if @school %> +We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>. +<% else %> +You have earned $<%= @distribution.amount %> for your TestDrive lesson with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> <% end%> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. -<% elsif distribution.is_normal? %> -You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>. -<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> -If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% elsif @distribution.is_normal? %> +<% if @school %> +We hope you enjoyed your lesson today with <%= @distribution.student.name %>. +<% else %> +You have earned $<%= @distribution.amount %> for your lesson with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> <% end%> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. -<% elsif distribution.is_monthly? %> -You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>. -<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> -If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% elsif @distribution.is_monthly? %> +<% if @school %> +We hope you enjoyed your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>. +<% else %> +You have earned $<%= @distribution.amount %> for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>. +<% end %> +<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %> <% end%> If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. <% else %> Unknown payment type. <% end %> -<% end %> Best Regards, JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb index bdd291b49..bcd797ffd 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb @@ -1,5 +1,12 @@ <% provide(:title, @subject) %> +

Hello <%= @name %>,

+ +<% if @school %> +

+ We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours. +

+<% else %>

<% if @card_declined %> When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! @@ -9,6 +16,7 @@ For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! <% end %>

+<% end %>
Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb index 8cc72bce5..a0ce26b67 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb @@ -1,5 +1,9 @@ <% provide(:title, @subject) %> +Hello <%= @name %>, +<% if @school %> + We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours. +<% else %> <% if @card_declined %> When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! <% elsif @card_expired %> @@ -7,6 +11,7 @@ When we tried to distribute a payment to you on <%= @bill_date %>, the charge wa <% else %> For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! <% end %> +<% end %> Best Regards, JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb index 7e76b8c0c..ab838c208 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb @@ -8,7 +8,7 @@ <% else %> This student has accepted your lesson request! <% end %> -

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it.

VIEW LESSON DETAILS diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb index 67590256e..f192b7b9c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb @@ -8,4 +8,4 @@ <%= @session_date %>

-

VIEW LESSON DETAILS

\ No newline at end of file +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 64b84ddbb..92d960b22 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -419,7 +419,7 @@ SQL end connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, video_sources) - JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id) + JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id, client_id) # connection.music_session_id = music_session.id # connection.as_musician = as_musician # connection.joining_session = true diff --git a/ruby/lib/jam_ruby/lib/stats.rb b/ruby/lib/jam_ruby/lib/stats.rb index a687f00a7..69020c5ce 100644 --- a/ruby/lib/jam_ruby/lib/stats.rb +++ b/ruby/lib/jam_ruby/lib/stats.rb @@ -1,5 +1,5 @@ -require 'influxdb' +#require 'influxdb' # monkey patch InfluxDB client to clear the queue when asked to stop =begin diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 30763c028..53e63e9e3 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -993,14 +993,16 @@ module JamRuby end # creates the chat message - def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel) + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel, lesson_session_id, purpose) chat_message = Jampb::ChatMessage.new( :sender_id => sender_id, :sender_name => sender_name, :msg => msg, :msg_id => msg_id, :created_at => created_at, - :channel => channel + :channel => channel, + :lesson_session_id => lesson_session_id, + :purpose => purpose ) if session_id diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index fc3ad2a8a..cca5d6eda 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -843,5 +843,9 @@ module JamRuby stats end + + def lesson_session + music_session.lesson_session + end end end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index aaf05fc35..0461490f7 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -357,7 +357,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base UPDATE affiliate_quarterly_payments SET closed = TRUE, closed_at = NOW() - WHERE year < #{year} OR quarter < #{quarter} + WHERE year < #{year} OR (year = #{year} AND quarter < #{quarter}) } ActiveRecord::Base.connection.execute(sql) diff --git a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb index 7955fc3a1..376578b9d 100644 --- a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb @@ -1,7 +1,7 @@ module JamRuby class AffiliatePaymentCharge < Charge - has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id + #has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id def distribution @distribution ||= teacher_payment.teacher_distribution @@ -36,15 +36,15 @@ module JamRuby end def do_send_notices - UserMailer.teacher_distribution_done(teacher_payment) + #UserMailer.teacher_distribution_done(teacher_payment) end def do_send_unable_charge - UserMailer.teacher_distribution_fail(teacher_payment) + #UserMailer.teacher_distribution_fail(teacher_payment) end def construct_description - teacher_payment.teacher_distribution.description + #teacher_payment.teacher_distribution.description end end diff --git a/ruby/lib/jam_ruby/models/artifact_update.rb b/ruby/lib/jam_ruby/models/artifact_update.rb index 9b671df91..080310564 100644 --- a/ruby/lib/jam_ruby/models/artifact_update.rb +++ b/ruby/lib/jam_ruby/models/artifact_update.rb @@ -4,7 +4,7 @@ module JamRuby DEFAULT_ENVIRONMENT = 'public' CLIENT_PREFIX = 'JamClient' - PRODUCTS = ["#{CLIENT_PREFIX}/Win32", "#{CLIENT_PREFIX}/MacOSX"] + PRODUCTS = ["#{CLIENT_PREFIX}/Win32", "#{CLIENT_PREFIX}/MacOSX", "#{CLIENT_PREFIX}/JamBlaster"] self.primary_key = 'id' attr_accessible :version, :uri, :sha1, :environment, :product, as: :admin diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb index 5af2b3b5a..83d9400ce 100644 --- a/ruby/lib/jam_ruby/models/charge.rb +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -1,6 +1,8 @@ module JamRuby class Charge < ActiveRecord::Base + belongs_to :user, class_name: "JamRuby::User" + validates :sent_billing_notices, inclusion: {in: [true, false]} def max_retries @@ -96,6 +98,7 @@ module JamRuby else self.billing_error_detail = e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace end + puts "Charge: unhandled exception #{billing_error_reason}, #{billing_error_detail}" self.save(validate: false) end diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 3c6b056db..a530c9d66 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -16,26 +16,44 @@ module JamRuby belongs_to :user belongs_to :music_session belongs_to :target_user, class_name: "JamRuby::User" - belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" validates :user, presence: true validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true, unless: :ignore_message_checks - def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_booking = nil) + def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_session = nil, purpose = nil) chat_msg = ChatMessage.new chat_msg.user_id = user.id chat_msg.music_session_id = music_session.id if music_session chat_msg.message = message chat_msg.channel = channel chat_msg.target_user = target_user - chat_msg.lesson_booking = lesson_booking + chat_msg.lesson_session = lesson_session + chat_msg.purpose = purpose - if lesson_booking + + if lesson_session chat_msg.ignore_message_checks = true + + if user.id == lesson_session.student.id + lesson_session.teacher_unread_messages = true + Notification.send_lesson_message('chat', lesson_session, false, message) + else + lesson_session.student_unread_messages = true + Notification.send_lesson_message('chat', lesson_session, true, message) + end + + lesson_session.save(validate: false) + + # a nil purpose means 'normal chat', which is the only time we should send an email + if !target_user.online? && purpose.nil? && message.present? + UserMailer.lesson_chat(chat_msg).deliver! + + end end if chat_msg.save - ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel + ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user end chat_msg end @@ -60,6 +78,11 @@ module JamRuby query = ChatMessage.where('music_session_id = ?', music_session_id) end + if params.has_key? (:lesson_session) + lesson_session_id = params[:lesson_session] + query = ChatMessage.where('lesson_session_id = ?', lesson_session_id) + end + query = query.offset(start).limit(limit).order('created_at DESC').includes([:user]) if query.length == 0 @@ -71,8 +94,9 @@ module JamRuby end end - def send_chat_msg(music_session, chat_msg, user, client_id, channel) + def send_chat_msg(music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user) music_session_id = music_session.id if music_session + lesson_session_id = lesson_session.id if lesson_session msg = @@message_factory.chat_message( music_session_id, @@ -81,14 +105,19 @@ module JamRuby chat_msg.message, chat_msg.id, chat_msg.created_at.utc.iso8601, - channel + channel, + lesson_session_id, + purpose ) if channel == 'session' @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) elsif channel == 'global' @@mq_router.publish_to_active_clients(msg) + elsif channel == 'lesson' + @@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) end + end end end diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb index 3f11a01e5..be18c5eb5 100644 --- a/ruby/lib/jam_ruby/models/instrument.rb +++ b/ruby/lib/jam_ruby/models/instrument.rb @@ -47,7 +47,7 @@ module JamRuby has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_instruments" def self.standard_list - return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC') + return Instrument.where('instruments.popularity > 0').order('instruments.description ASC') end def self.jam_track_list diff --git a/ruby/lib/jam_ruby/models/language.rb b/ruby/lib/jam_ruby/models/language.rb index bb5d64316..ae249ff8e 100644 --- a/ruby/lib/jam_ruby/models/language.rb +++ b/ruby/lib/jam_ruby/models/language.rb @@ -3,5 +3,10 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:name, :description] has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_languages" + + def self.english_sort + languages = Language.order(:description) + languages.sort_by { |l| [ l.id == 'EN' ? 0 : 1, l.description] } + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 384009aaa..7e884fef4 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -9,7 +9,7 @@ module JamRuby @@log = Logging.logger[LessonBooking] - attr_accessor :accepting, :countering, :countered_slot, :countered_lesson + attr_accessor :accepting, :countering, :canceling, :countered_slot, :countered_lesson STATUS_REQUESTED = 'requested' STATUS_CANCELED = 'canceled' @@ -37,12 +37,12 @@ module JamRuby belongs_to :teacher, class_name: "JamRuby::User" belongs_to :accepter, class_name: "JamRuby::User" belongs_to :canceler, class_name: "JamRuby::User" - belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking - belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking - - has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" - has_many :lesson_sessions, class_name: "JamRuby::LessonSession" - has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase" + 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" + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy + has_many :lesson_sessions, class_name: "JamRuby::LessonSession", :dependent => :destroy + has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase", :dependent => :destroy validates :user, presence: true validates :teacher, presence: true @@ -62,7 +62,9 @@ module JamRuby validate :validate_lesson_booking_slots validate :validate_lesson_length validate :validate_payment_style + validate :validate_uncollectables, on: :create validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling before_save :before_save @@ -85,7 +87,7 @@ module JamRuby end def after_create - if card_presumed_ok && !sent_notices + if (card_presumed_ok || school_on_school?) && !sent_notices send_notices end end @@ -136,6 +138,9 @@ module JamRuby def next_lesson if recurring session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + if session.nil? + session = lesson_sessions[0] + end LessonSession.find(session.id) if session else lesson_sessions[0] @@ -272,7 +277,7 @@ module JamRuby times << time end end - times + { times: times, session: sessions.first } end def determine_needed_sessions(sessions) @@ -390,6 +395,14 @@ module JamRuby self.accepting = false end + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + self.canceling = false + end + def send_notices UserMailer.student_lesson_request(self).deliver UserMailer.teacher_lesson_request(self).deliver @@ -398,11 +411,26 @@ module JamRuby self.save end + def resolved_test_drive_package + result = nil + purchase = student.most_recent_test_drive_purchase + if purchase + # for lessons already packaged + result = purchase.lesson_package_type + else + # for unbooked lessons + result = student.desired_package + end + if result.nil? + result = LessonPackageType.test_drive_4 + end + result + end def lesson_package_type if is_single_free? LessonPackageType.single_free elsif is_test_drive? - LessonPackageType.test_drive + resolved_test_drive_package elsif is_normal? LessonPackageType.single end @@ -439,19 +467,32 @@ module JamRuby if is_single_free? 0 elsif is_test_drive? - LessonPackageType.test_drive.price + resolved_test_drive_package.price elsif is_normal? teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY) end end - def distribution_price_in_cents + def distribution_price_in_cents(target) if is_single_free? 0 elsif is_test_drive? 10 * 100 elsif is_normal? - booked_price * 100 + if is_monthly_payment? + raise "not a LessonPackagePurchase: #{target.inspect}" if !target.is_a?(LessonPackagePurchase) + + today = Date.today + + start_date = Date.new(target.year, target.month, 1) + if today.year == target.year && today.month == target.month + # we are in the month being billed. we should set the start date based on today + start_date = today + end + LessonSessionMonthlyPrice.price(self, start_date) * 100 + else + booked_price * 100 + end end end @@ -500,41 +541,49 @@ module JamRuby def approved_before? !self.accepter_id.nil? end + def cancel(canceler, other, message) + self.canceling = true self.active = false self.status = STATUS_CANCELED self.cancel_message = message self.canceler = canceler success = save if success + lesson_sessions.past_cancel_window.each do |lesson_session| + lesson_session = LessonSession.find(lesson_session.id) # because .upcoming creates ReadOnly records + lesson_session.cancel_lesson(canceler, message) + if !lesson_session.save + return lesson_session + end + end if approved_before? # just tell both people it's cancelled, to act as confirmation Notification.send_lesson_message('canceled', next_lesson, false) Notification.send_lesson_message('canceled', next_lesson, true) UserMailer.student_lesson_booking_canceled(self, message).deliver UserMailer.teacher_lesson_booking_canceled(self, message).deliver - chat_message_prefix = "Lesson Canceled" + purpose = "Lesson Canceled" else if canceler == student # if it's the first time acceptance student canceling, we call it a 'cancel' Notification.send_lesson_message('canceled', next_lesson, false) UserMailer.teacher_lesson_booking_canceled(self, message).deliver - chat_message_prefix = "Lesson Canceled" + purpose = "Lesson Canceled" else # if it's the first time acceptance teacher, it was declined UserMailer.student_lesson_booking_declined(self, message).deliver Notification.send_lesson_message('declined', next_lesson, true) - chat_message_prefix = "Lesson Declined" + purpose = "Lesson Declined" end end - chat_message = message.nil? ? chat_message_prefix : "#{chat_message_prefix}: #{message}" - msg = ChatMessage.create(canceler, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, other, self) - + message = '' if message.nil? + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, next_lesson, purpose) end - success + self end def card_approved @@ -555,7 +604,7 @@ module JamRuby #end elsif is_test_drive? if user.has_requested_test_drive?(teacher) && !user.admin - errors.add(:user, "has a requested TestDrive with this teacher") + errors.add(:user, "have a requested TestDrive with this teacher") end if !user.has_test_drives? && !user.can_buy_test_drive? errors.add(:user, "have no remaining test drives") @@ -603,6 +652,15 @@ module JamRuby end end + def validate_uncollectables + if user.uncollectables.count > 0 + errors.add(:user, 'have unpaid lessons.') + end + end + + def school_owned? + !!school + end def self.book_free(user, teacher, lesson_booking_slots, description) self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) @@ -632,6 +690,9 @@ module JamRuby lesson_booking.payment_style = payment_style lesson_booking.description = description lesson_booking.status = STATUS_REQUESTED + if lesson_booking.teacher && lesson_booking.teacher.teacher.school + lesson_booking.school = lesson_booking.teacher.teacher.school + end # two-way association slots, for before_validation loic in slot to work lesson_booking.lesson_booking_slots = lesson_booking_slots @@ -641,20 +702,24 @@ module JamRuby end if lesson_booking_slots if lesson_booking.save - msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking) + description = '' if description.nil? + msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking.lesson_sessions[0], 'Lesson Requested') end end lesson_booking end def self.unprocessed(current_user) - LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false) + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where('school_id IS NULL') end def self.requested(current_user) LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED) end + def school_on_school? + teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id) + end def self.find_bookings_needing_sessions(minimum_start_time) MusicSession.select([:lesson_booking_id]).joins(:lesson_session => :lesson_booking).where("lesson_bookings.active = true").where('lesson_bookings.recurring = true').where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).group(:lesson_booking_id).having('count(lesson_booking_id) < 2') @@ -806,7 +871,6 @@ module JamRuby end end - def home_url APP_CONFIG.external_root_url + "/client#/jamclass" end diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb index 758bbc319..e0c8e9efa 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -73,25 +73,44 @@ module JamRuby def scheduled_times(needed_sessions, minimum_start_time) + #puts "NEEDED SESSIONS #{needed_sessions} #{minimum_start_time}" times = [] week_offset = 0 needed_sessions.times do |i| candidate = scheduled_time(i + week_offset) + #puts "#{i}: candidate #{candidate} week_offset:#{week_offset}" if day_of_week && candidate <= minimum_start_time # move it up a week week_offset += 1 candidate = scheduled_time(i + week_offset) + #puts "retry #1 #{candidate}" # sanity check if candidate <= minimum_start_time week_offset += 1 candidate = scheduled_time(i + week_offset) + #puts "retry #2 #{candidate}" if candidate <= minimum_start_time - raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}" + + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #3 #{candidate}" + if candidate <= minimum_start_time + + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + #puts "retry #4 #{candidate}" + if candidate <= minimum_start_time + raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}" + end + end end + end end times << candidate diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index 7d0e98e4d..66266028f 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -26,23 +26,26 @@ module JamRuby def validate_test_drive if user - if !user.can_buy_test_drive? + if lesson_package_type.is_test_drive? && !user.can_buy_test_drive? errors.add(:user, "can not buy test drive right now because you have already purchased it within the last year") end end end def create_charge - self.lesson_payment_charge = LessonPaymentCharge.new - lesson_payment_charge.amount_in_cents = 0 - lesson_payment_charge.fee_in_cents = 0 - lesson_payment_charge.lesson_package_purchase = self - lesson_payment_charge.save! + if !school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.user = user + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_package_purchase = self + lesson_payment_charge.save! + end end def add_test_drives if self.lesson_package_type.is_test_drive? - new_test_drives = user.remaining_test_drives + 4 + new_test_drives = user.remaining_test_drives + lesson_package_type.test_drive_count User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) user.remaining_test_drives = new_test_drives end @@ -71,17 +74,19 @@ module JamRuby if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + # price should always match the teacher_distribution, if there is one + purchase.price = purchase.teacher_distribution.amount_in_cents / 100 end else purchase.recurring = false end if lesson_booking - purchase.lesson_package_type = lesson_booking.lesson_package_type - purchase.price = lesson_booking.booked_price # lesson_package_type.booked_price(lesson_booking) + purchase.lesson_package_type = lesson_package_type ? lesson_package_type : lesson_booking.lesson_package_type + purchase.price = lesson_booking.booked_price if purchase.price.nil? else purchase.lesson_package_type = lesson_package_type - purchase.price = lesson_package_type.price + purchase.price = lesson_package_type.price if purchase.price.nil? end purchase.save @@ -100,6 +105,9 @@ module JamRuby description(lesson_booking) end + def timed_description + "Lessons for the month of #{self.month_name} with #{self.lesson_booking.student.name}" + end def month_name if recurring @@ -113,11 +121,21 @@ module JamRuby user end + def school_on_school? + teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id) + end + def bill_monthly(force = false) - lesson_payment_charge.charge(force) - if lesson_payment_charge.billed + if school_on_school? + success = true + else + lesson_payment_charge.charge(force) + success = lesson_payment_charge.billed + end + + if success self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb index d60aaa14a..d151e1355 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_type.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -7,21 +7,29 @@ module JamRuby PRODUCT_TYPE = 'LessonPackageType' SINGLE_FREE = 'single-free' - TEST_DRIVE = 'test-drive' + TEST_DRIVE_4 = 'test-drive' + TEST_DRIVE_2 = 'test-drive-2' + TEST_DRIVE_1 = 'test-drive-1' SINGLE = 'single' LESSON_PACKAGE_TYPES = [ SINGLE_FREE, - TEST_DRIVE, + TEST_DRIVE_4, + TEST_DRIVE_2, + TEST_DRIVE_1, SINGLE ] + has_many :user_desired_packages, class_name: "JamRuby::User", :foreign_key => "lesson_package_type_id", inverse_of: :desired_package validates :name, presence: true validates :description, presence: true validates :price, presence: true validates :package_type, presence: true, inclusion: {in: LESSON_PACKAGE_TYPES} + def self.test_drive_package_ids + [TEST_DRIVE_4, TEST_DRIVE_2, TEST_DRIVE_1] + end def self.monthly LessonPackageType.find(MONTHLY) end @@ -30,8 +38,16 @@ module JamRuby LessonPackageType.find(SINGLE_FREE) end - def self.test_drive - LessonPackageType.find(TEST_DRIVE) + def self.test_drive_4 + LessonPackageType.find(TEST_DRIVE_4) + end + + def self.test_drive_2 + LessonPackageType.find(TEST_DRIVE_2) + end + + def self.test_drive_1 + LessonPackageType.find(TEST_DRIVE_1) end def self.single @@ -42,17 +58,21 @@ module JamRuby if is_single_free? 0 elsif is_test_drive? - LessonPackageType.test_drive.price + 10.00 elsif is_normal? lesson_booking.booked_price #teacher.teacher.booking_price(lesson_booking.lesson_length, lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_SINGLE) end end + def test_drive_count + package_type["test-drive-".length, 1].to_i + end + def description(lesson_booking) if is_single_free? "Single Free Lesson" elsif is_test_drive? - "Test Drive" + "Test Drive (#{test_drive_count})" elsif is_normal? if lesson_booking.recurring "Recurring #{lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_WEEKLY ? "Weekly" : "Monthly"} #{lesson_booking.lesson_length}m" @@ -71,7 +91,7 @@ module JamRuby end def is_test_drive? - id == TEST_DRIVE + id.start_with?('test-drive') end def is_normal? @@ -86,8 +106,12 @@ module JamRuby def plan_code if package_type == SINGLE_FREE "lesson-package-single-free" - elsif package_type == TEST_DRIVE - "lesson-package-test-drive" + elsif package_type == 'test-drive-4' + "lesson-package-test-drive-4" + elsif package_type == TEST_DRIVE_2 + "lesson-package-test-drive-2" + elsif package_type == TEST_DRIVE_1 + "lesson-package-test-drive-1" elsif package_type == SINGLE "lesson-package-single" else diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb index f68e84fc6..e1cda471a 100644 --- a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -9,7 +9,7 @@ module JamRuby end def charged_user - @charged_user ||= target.student + user end def resolve_target @@ -31,6 +31,10 @@ module JamRuby charged_user end + def teacher + target.teacher + end + def is_lesson? !lesson_session.nil? end @@ -85,5 +89,13 @@ module JamRuby end end end + + def description + target.timed_description + end + + def expected_price_in_cents + target.lesson_booking.distribution_price_in_cents(target) + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index dec9512fd..c85624e77 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -5,13 +5,13 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:cancel_message] - attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling + attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling, :assigned_student @@log = Logging.logger[LessonSession] - delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge - delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, to: :lesson_booking + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, to: :lesson_booking delegate :pretty_scheduled_start, to: :music_session @@ -30,16 +30,18 @@ module JamRuby LESSON_TYPE_TEST_DRIVE = 'test-drive' LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE] - has_one :music_session, class_name: "JamRuby::MusicSession" + has_one :music_session, class_name: "JamRuby::MusicSession", :dependent => :destroy belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons belongs_to :canceler, class_name: "JamRuby::User", foreign_key: :canceler_id belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" - belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id + belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id, :dependent => :destroy belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id - belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson, :dependent => :destroy has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id" + has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" validates :duration, presence: true, numericality: {only_integer: true} @@ -69,10 +71,13 @@ module JamRuby scope :suspended, -> { where(status: STATUS_SUSPENDED) } scope :completed, -> { where(status: STATUS_COMPLETED) } scope :missed, -> { where(status: STATUS_MISSED) } + scope :upcoming, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', Time.now) } + scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now) } def create_charge - if !is_test_drive? + if !school_on_school? && !is_test_drive? && !is_monthly_payment? self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.user = @assigned_student lesson_payment_charge.amount_in_cents = 0 lesson_payment_charge.fee_in_cents = 0 lesson_payment_charge.lesson_session = self @@ -96,23 +101,52 @@ module JamRuby self.save end + def music_session_id + music_session.id + end + def self.hourly_check analyse_sessions complete_sessions end + def self.minutely_check + upcoming_sessions_reminder + end + def self.analyse_sessions - MusicSession.joins(lesson_session: :lesson_booking).where('session_removed_at IS NOT NULL').where('analysed = false').each do |music_session| + MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = false').each do |music_session| lession_session = music_session.lesson_session lession_session.analyse end end def self.complete_sessions - MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('session_removed_at IS NOT NULL').where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| + # this will find any paid session (recurring monthly paid, recurring single paid, single paid) + MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| lession_session = music_session.lesson_session lession_session.session_completed end + + # test drives don't have a lesson_payment_charge, so we don't join against them + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session| + lession_session = music_session.lesson_session + lession_session.session_completed + end + end + + def self.upcoming_sessions_reminder + now = Time.now + half_hour_from_now = 30.minutes.from_now + if Time.zone + now = Time.zone.local_to_utc(now) + half_hour_from_now = Time.zone.local_to_utc(half_hour_from_now) + end + + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where('sent_starting_notice = false').where('(scheduled_start > ? and scheduled_start < ?)', now, half_hour_from_now).each do |music_session| + lession_session = music_session.lesson_session + lession_session.send_starting_notice + end end def analyse @@ -122,12 +156,14 @@ module JamRuby analysis = LessonSessionAnalyser.analyse(self) - self.analysis = analysis_to_json(analysis) + self.analysis = LessonSession.analysis_to_json(analysis) self.success = analysis[:bill] self.analysed_at = Time.now self.analysed = true - if lesson_booking.requires_teacher_distribution?(self) + self.status = STATUS_COMPLETED + + if success && lesson_booking.requires_teacher_distribution?(self) self.teacher_distribution = TeacherDistribution.create_for_lesson(self) end @@ -137,12 +173,19 @@ module JamRuby end end + def billed + if lesson_booking.is_test_drive? + false + else + lesson_payment_charge.billed + end + end def amount_charged lesson_payment_charge.amount_in_cents / 100.0 end - def analysis_to_json(analysis) + def self.analysis_to_json(analysis, preserve_object = false) json = {} analysis.each do |k, v| @@ -160,7 +203,19 @@ module JamRuby json[k] = v end end - json.to_json + if preserve_object + json + else + json.to_json + end + end + + def send_starting_notice + UserMailer.lesson_starting_soon_student(self).deliver! + UserMailer.lesson_starting_soon_teacher(self).deliver! + + self.sent_starting_notice = true + self.save(validate: false) end def session_completed @@ -186,10 +241,14 @@ module JamRuby end def bill_lesson + if school_on_school? + success = true + else + lesson_payment_charge.charge + success = lesson_payment_charge.billed + end - lesson_payment_charge.charge - - if lesson_payment_charge.billed + if success self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true @@ -202,13 +261,13 @@ module JamRuby def test_drive_completed distribution = teacher_distribution - if distribution # not all lessons/payment charges have a distribution - distribution.ready = true - distribution.save(validate: false) - end if !sent_notices if success + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end student.test_drive_succeeded(self) else student.test_drive_failed(self) @@ -242,9 +301,12 @@ module JamRuby end else if lesson_booking.is_monthly_payment? - # bad session; just poke user if !sent_notices - UserMailer.monthly_recurring_no_bill(self).deliver + if !school_on_school? + # bad session; just poke user + UserMailer.monthly_recurring_no_bill(self).deliver + end + self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true @@ -254,8 +316,11 @@ module JamRuby else if !sent_notices - # bad session; just poke user - UserMailer.student_weekly_recurring_no_bill(student, self).deliver + if !school_on_school? + # bad session; just poke user + UserMailer.student_lesson_normal_no_bill(self).deliver + end + self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true @@ -272,8 +337,11 @@ module JamRuby bill_lesson else if !sent_notices - UserMailer.student_lesson_normal_no_bill(self).deliver - UserMailer.teacher_lesson_no_bill(self).deliver + if !school_on_school? + UserMailer.student_lesson_normal_no_bill(self).deliver + UserMailer.teacher_lesson_normal_no_bill(self).deliver + end + self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true @@ -314,10 +382,6 @@ module JamRuby status == STATUS_COMPLETED end - def is_missed? - status == STATUS_MISSED - end - def is_approved? status == STATUS_APPROVED end @@ -330,6 +394,14 @@ module JamRuby status == STATUS_COUNTERED end + def analysis_json + @parsed_analysis || analysis ? JSON.parse(analysis) : nil + end + + def school_on_school? + teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id) + end + def validate_creating if !is_requested? && !is_approved? self.errors.add(:status, "is not valid for a new lesson session.") @@ -378,6 +450,7 @@ module JamRuby lesson_session.teacher = booking.teacher lesson_session.status = booking.status lesson_session.slot = booking.default_slot + lesson_session.assigned_student = booking.student if booking.is_test_drive? lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase end @@ -393,21 +466,31 @@ module JamRuby music_session.creator end + def student_id + music_session.creator.id + end + def self.index(user, params = {}) limit = params[:per_page] limit ||= 100 limit = limit.to_i - query = LessonSession.joins(:music_session).joins(music_session: :creator) - query = query.includes([:teacher, :music_session]) + query = LessonSession.unscoped.joins([:music_session, :lesson_booking]).joins(music_session: :creator) + #query = query.includes([:teacher, :music_session]) + query = query.includes([:music_session]) query = query.order('music_sessions.scheduled_start DESC') - if params[:as_teacher] - query = query.where('lesson_sessions.teacher_id = ?', user.id) + if params[:as_teacher].present? + if params[:as_teacher] + query = query.where('lesson_sessions.teacher_id = ?', user.id) + else + query = query.where('music_sessions.user_id = ?', user.id) + end else - query = query.where('music_sessions.user_id = ?', user.id) + query = query.where('(lesson_sessions.teacher_id = ? or music_sessions.user_id = ?)', user.id, user.id) end + query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?)', user.id) current_page = params[:page].nil? ? 1 : params[:page].to_i next_page = current_page + 1 @@ -451,6 +534,17 @@ module JamRuby time.nil? ? nil : attempt end + def school_owner_id + school = teacher.teacher.school + if school + school.owner.id + end + end + + def access?(user) + user.id == music_session.user_id || user.id == teacher.id || user.id == school_owner_id + end + # teacher accepts the lesson def accept(params) response = self @@ -480,8 +574,8 @@ module JamRuby end UserMailer.student_lesson_accepted(self, message, slot).deliver UserMailer.teacher_lesson_accepted(self, message, slot).deliver - chat_message = message ? "Lesson Approved: '#{message}'" : "Lesson Approved" - msg = ChatMessage.create(teacher, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Approved") Notification.send_jamclass_invitation_teacher(music_session, teacher) Notification.send_student_jamclass_invitation(music_session, student) Notification.send_lesson_message('accept', self, true) @@ -501,8 +595,8 @@ module JamRuby response = lesson_booking raise ActiveRecord::Rollback end - chat_message = message ? "All Lesson Times Updated: '#{message}'" : "All Lesson Times Updated" - msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "All Lesson Times Updated") Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept' UserMailer.student_lesson_update_all(self, message, slot).deliver UserMailer.teacher_lesson_update_all(self, message, slot).deliver @@ -515,8 +609,8 @@ module JamRuby puts("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") raise ActiveRecord::Rollback end - chat_message = message ? "Lesson Updated Time Approved: '#{message}'" : "Lesson Updated Time Approved" - msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "Lesson Updated Time Approved") UserMailer.student_lesson_accepted(self, message, slot).deliver UserMailer.teacher_lesson_accepted(self, message, slot).deliver end @@ -551,25 +645,42 @@ module JamRuby self.counter_slot = slot end if self.save - if update_all - if !lesson_booking.counter(self, proposer, slot) - response = lesson_booking - raise ActiveRecord::Rollback - end + if update_all && !lesson_booking.counter(self, proposer, slot) + response = lesson_booking + raise ActiveRecord::Rollback end else response = self raise ActiveRecord::Rollback end - - msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "New Time Proposed") Notification.send_lesson_message('counter', self, slot.is_teacher_created?) end response end + def cancel_lesson(canceler, message) + canceled_by_student = canceler == student + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + self.canceling = true + + if canceled_by_student + self.student_canceled = true + self.student_canceled_at = Time.now + self.student_canceled_reason = message + self.student_short_canceled = 24.hours.from_now > scheduled_start + else + self.teacher_canceled = true + self.teacher_canceled_at = Time.now + self.teacher_canceled_reason = message + self.teacher_short_canceled = 24.hours.from_now > scheduled_start + end + end # teacher accepts the lesson def cancel(params) @@ -577,39 +688,39 @@ module JamRuby LessonSession.transaction do canceler = params[:canceler] - other = canceler == teacher ? student : teacher + canceled_by_student = canceler == student + other = canceled_by_student ? teacher : student message = params[:message] + message = '' if message.nil? - if params[:update_all].present? + if lesson_booking.recurring update_all = params[:update_all] else - update_all = !lesson_booking.recurring + update_all = true end + if lesson_booking.is_test_drive? + student.test_drive_declined(self) + end - self.status = STATUS_CANCELED - self.cancel_message = message - self.canceler = canceler - self.canceling = true - - if self.save - if update_all - if !lesson_booking.cancel(canceler, other, message) - response = lesson_booking - raise ActiveRecord::Rollback - end - else - msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, lesson_booking) - Notification.send_lesson_message('canceled', self, false) - Notification.send_lesson_message('canceled', self, true) - UserMailer.student_lesson_canceled(self, message).deliver - UserMailer.teacher_lesson_canceled(self, message).deliver + if update_all + response = lesson_booking.cancel(canceler, other, message) + if response.errors.any? + raise ActiveRecord::Rollback end else - response = self - raise ActiveRecord::Rollback - end + cancel_lesson(canceler, message) + if !save + response = self + raise ActiveRecord::Rollback + end + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, self, "Lesson Canceled") + Notification.send_lesson_message('canceled', self, false) + Notification.send_lesson_message('canceled', self, true) + UserMailer.student_lesson_canceled(self, message).deliver + UserMailer.teacher_lesson_canceled(self, message).deliver + end end response @@ -619,6 +730,19 @@ module JamRuby lesson_booking.lesson_package_type.description(lesson_booking) end + def timed_description + if is_test_drive? + "TestDrive session with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + else + if self.lesson_booking.is_monthly_payment? + "Monthly Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + else + "Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}" + end + end + end + + def stripe_description(lesson_booking) description(lesson_booking) end @@ -638,5 +762,9 @@ module JamRuby def admin_url APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + id end + + def chat_url + APP_CONFIG.external_root_url + "/client#/jamclass/chat-dialog/d1=lesson_" + id + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb index d1883a77d..57fb611e8 100644 --- a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -1,7 +1,7 @@ module JamRuby class LessonSessionAnalyser - SUCCESS = 'success' + SUCCESS = 'success' SESSION_ONGOING = 'session_ongoing' THRESHOLD_MET = 'threshold_met' WAITED_CORRECTLY = 'waited_correctly' @@ -16,6 +16,7 @@ module JamRuby STUDENT_NOT_THERE_WHEN_JOINED = 'student_not_there_when_joined' JOINED_LATE = 'did_not_join_on_time' NO_SHOW = 'no_show' + NEITHER_SHOW = 'neither_show' # what are the potential results? @@ -42,7 +43,7 @@ module JamRuby # reason: 'both_fault' - def self.analyse(lesson_session) + def self.analyse(lesson_session, force = false) reason = nil teacher = nil student = nil @@ -62,26 +63,25 @@ module JamRuby teacher_ranges = merge_overlapping_ranges(all_teacher_ranges) intersecting = intersecting_ranges(student_ranges, teacher_ranges) - student_analysis = analyse_intersection(lesson_session, student_ranges) teacher_analysis = analyse_intersection(lesson_session, teacher_ranges) together_analysis = analyse_intersection(lesson_session, intersecting) # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest - if music_session.session_removed_at.nil? + if !force && !((music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now) reason = SESSION_ONGOING bill = false else - if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? - # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. - teacher = LATE_CANCELLATION - bill = false - elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? - # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. - student = LATE_CANCELLATION - bill = true - elsif together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes + #if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. + # teacher = LATE_CANCELLATION + # bill = false + #elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. + # student = LATE_CANCELLATION + # bill = true + if together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes bill = true reason = SUCCESS elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] @@ -98,8 +98,17 @@ module JamRuby student = JOINED_LATE bill = true end + else + if teacher_analysis[:no_show] + teacher = NO_SHOW + elsif !teacher_analysis[:joined_on_time] + teacher = JOINED_LATE + elsif !teacher_analysis[:waited_correctly] + teacher = MINIMUM_TIME_NOT_MET + end end + end if reason.nil? @@ -107,6 +116,8 @@ module JamRuby reason = STUDENT_FAULT elsif teacher reason = TEACHER_FAULT + else + reason = NEITHER_SHOW end end @@ -150,7 +161,8 @@ module JamRuby def self.analyse_intersection(lesson_session, ranges) - start = lesson_session.scheduled_start + # be sure to call .to_time on any ActiveRecord time, because we get a ton of deprecation warninsg about Time#succ if you use ActiveSupport:: TimeZone + start = lesson_session.scheduled_start.to_time planned_duration_seconds = lesson_session.duration * 60 end_time = start + planned_duration_seconds @@ -286,7 +298,7 @@ module JamRuby end def self.ranges_overlap?(a, b) - a.include?(b.begin) || b.include?(a.begin) + a.cover?(b.begin) || b.cover?(a.begin) end def self.merge_ranges(a, b) diff --git a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb index 068721a2b..b2eae100c 100644 --- a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb +++ b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb @@ -6,7 +6,19 @@ module JamRuby raise "lesson_booking is not monthly paid #{lesson_booking.admin_url}" if !lesson_booking.is_monthly_payment? - times = lesson_booking.predicted_times_for_month(start_day.year, start_day.month) + data = lesson_booking.predicted_times_for_month(start_day.year, start_day.month) + + times = data[:times] + session = data[:session] + + true_start = start_day + if session + # if there is already a session for the month, that is the real star + true_start = session.scheduled_start.to_date + end + + # filter out anything before the start day + times.select! { |time| time.to_date >= true_start } result = nil if times.length == 0 diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index d63c798db..d03d8ac67 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -22,6 +22,7 @@ module JamRuby CREATE_TYPE_IMMEDIATE = 'immediately' CREATE_TYPE_QUICK_START = 'quick-start' CREATE_TYPE_LESSON = 'lesson' + CREATE_TYPE_QUICK_PUBLIC = 'quick-public' attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed @@ -323,11 +324,29 @@ module JamRuby # let session be restarted for up to 2 hours after finishing session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)" - query = MusicSession.where("music_sessions.canceled = FALSE") + query = MusicSession.joins( + %Q{ + LEFT OUTER JOIN + invitations + ON + music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}' + } + ) + query = query.where("music_sessions.canceled = FALSE") query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public - query = query.where("music_sessions.user_id = '#{user.id}'") + #query = query.where("music_sessions.user_id = '#{user.id}' OR invitations.id IS NOT NULL") + query = query.where("music_sessions.id in ( + select distinct(rs.music_session_id) + from rsvp_slots rs + where rs.id in ( + select rrrs.rsvp_slot_id + from rsvp_requests rr + inner join rsvp_requests_rsvp_slots rrrs on rr.id = rrrs.rsvp_request_id + where rr.user_id = '#{user.id}'AND rrrs.chosen = true + ) + ) OR invitations.id IS NOT NULL OR music_sessions.user_id = '#{user.id}'") query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}") - query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}'") + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')") query = query.order("music_sessions.scheduled_start ASC") query @@ -339,7 +358,7 @@ module JamRuby filter_approved = only_approved ? 'AND rrrs.chosen = true' : '' MusicSession.where(%Q{music_sessions.canceled = FALSE AND - (music_sessions.create_type is NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}') AND + (music_sessions.create_type is NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')) AND (music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND music_sessions.id in ( select distinct(rs.music_session_id) @@ -760,7 +779,7 @@ module JamRuby query = query.offset(offset) query = query.limit(limit) - query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE) + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE, MusicSession::CREATE_TYPE_QUICK_PUBLIC) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? @@ -875,7 +894,7 @@ SQL result end - def scheduled_start_date + def scheduled_start_date if self.scheduled_start_time.blank? "" else @@ -936,20 +955,16 @@ SQL end duration end - # should create a timestamp like: - # - # with_timezone = TRUE - # Tuesday, April 29, 8:00-9:00 PM TIMEZONE (where TIMEZONE is the TIMEZONE defined in the MusicSession when it was created) - # - # with_timezone = FALSE - # Thursday, July 10 - 10:00pm - # this should be in a helper - def pretty_scheduled_start(with_timezone = true) + + def pretty_scheduled_start(with_timezone = true, shorter = false) if scheduled_start && scheduled_duration start_time = scheduled_start timezone_display = 'UTC' + utc_offset_display = '00:00' tz_identifier, tz_display = MusicSession.split_timezone(timezone) + short_tz = 'GMT' + begin tz = TZInfo::Timezone.get(tz_identifier) rescue Exception => e @@ -960,6 +975,15 @@ SQL begin start_time = tz.utc_to_local(scheduled_start.utc) timezone_display = tz_display + utc_offset_hours = tz.current_period.utc_total_offset / (60*60) + hour = sprintf '%02d', utc_offset_hours.abs + minutes = sprintf '%02d', ((tz.current_period.utc_total_offset.abs % 3600) / 3600) * 60 + utc_offset_display = "#{utc_offset_hours < 0 ? '-' : ' '}#{hour}:#{minutes}" + short_tz = start_time.strftime("%Z") + if short_tz == 'UTC' + short_tz = 'GMT' + end + rescue Exception => e @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") puts "unable to convert #{e}" @@ -969,7 +993,11 @@ SQL duration = safe_scheduled_duration end_time = start_time + duration if with_timezone - "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" + if shorter + "#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{short_tz}#{utc_offset_display})" + else + "#{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}" end diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index 2e7bd25d4..c9b042d8b 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -26,7 +26,9 @@ module JamRuby end def range - Range.new(created_at, session_removed_at || Time.now) + # to_time to not use ActiveSupport::TimeWithZone + session_removed_at_time = session_removed_at.to_time if session_removed_at + Range.new(created_at.to_time, session_removed_at_time || Time.now.to_time) end def music_session @@ -38,9 +40,9 @@ module JamRuby end def self.save(music_session_id, user_id, client_id, tracks) - return true if 0 < self.where(:music_session_id => music_session_id, - :user_id => user_id, - :client_id => client_id).count + return true if 0 < self.where(:music_session_id => music_session_id, + :user_id => user_id, + :client_id => client_id).where('session_removed_at is NULL').count session_user_history = MusicSessionUserHistory.new session_user_history.music_session_id = music_session_id session_user_history.user_id = user_id @@ -58,10 +60,12 @@ module JamRuby (end_time - self.created_at) / 60.0 end - def self.join_music_session(user_id, session_id) + def self.join_music_session(user_id, session_id, client_id) hist = self .where(:user_id => user_id) .where(:music_session_id => session_id) + .where(:client_id => client_id) + .where('session_removed_at IS NULL') .limit(1) .first hist.start_history if hist diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index d47ecff00..05298a1f5 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -63,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed, msg: message}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -135,6 +135,7 @@ module JamRuby session = options[:session] purpose = options[:purpose] student_directed = options[:student_directed] + msg = options[:msg] name, band_name = "" unless user.nil? @@ -271,6 +272,8 @@ module JamRuby end elsif purpose == 'reschedule' 'A lesson reschedule has been requested' + elsif purpose == 'chat' + notification_msg = "Lesson Message: #{msg}" end return notification_msg @@ -404,7 +407,7 @@ module JamRuby end end - def send_lesson_message(purpose, lesson_session, student_directed) + def send_lesson_message(purpose, lesson_session, student_directed, msg = nil) notification = Notification.new notification.description = NotificationTypes::LESSON_MESSAGE @@ -422,9 +425,11 @@ module JamRuby notification.session_id = lesson_session.music_session.id notification.lesson_session_id = lesson_session.id - notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose}) + notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose, msg: msg}) - #notification.message = notification_msg + if purpose == 'chat' + notification.message = msg + end notification.save @@ -756,7 +761,7 @@ module JamRuby end begin - UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver + #UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver rescue => e @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{music_session.lesson_session.teacher.email} #{e}") end @@ -796,7 +801,7 @@ module JamRuby end begin - UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver + #UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver rescue => e @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{student.email} #{e}") end @@ -1094,7 +1099,7 @@ module JamRuby # start in less than 24 hours, and haven't been # notified for a particular interval yet: def send_session_reminders - MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS')").each do |candidate_session| + MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS') AND lesson_session_id IS NULL").each do |candidate_session| tm = candidate_session.scheduled_start if (tm>(12.hours.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY)) # Send 24 hour reminders: diff --git a/ruby/lib/jam_ruby/models/payment_history.rb b/ruby/lib/jam_ruby/models/payment_history.rb index 4862e4dd1..ab6013d62 100644 --- a/ruby/lib/jam_ruby/models/payment_history.rb +++ b/ruby/lib/jam_ruby/models/payment_history.rb @@ -5,6 +5,7 @@ module JamRuby belongs_to :sale belongs_to :recurly_transaction_web_hook + belongs_to :charge def self.index(user, params = {}) @@ -14,7 +15,7 @@ module JamRuby limit = limit.to_i query = PaymentHistory.limit(limit) - .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[]) + .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[], charge:[]) .where(user_id: user.id) .where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'") .order('created_at DESC') diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 5cbd4f9d7..d3827ed84 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -208,8 +208,8 @@ module JamRuby free && non_free end - def self.purchase_test_drive(current_user, booking = nil) - self.purchase_lesson(current_user, booking, LessonPackageType.test_drive) + def self.purchase_test_drive(current_user, lesson_package_type, booking = nil) + self.purchase_lesson(current_user, booking, lesson_package_type) end def self.purchase_normal(current_user, booking) @@ -273,6 +273,7 @@ module JamRuby purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil? if purchase.errors.any? + puts "purchase errors #{purchase.errors.inspect}" price_info = {} price_info[:purchase] = purchase return price_info diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 61e753bbe..f5ca99b80 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -43,14 +43,17 @@ module JamRuby JamTrack.find_by_id(product_id) elsif product_type == GIFTCARD GiftCardType.find_by_id(product_id) + elsif product_type == LESSON + lesson_package_purchase else + raise 'unsupported product type' end end def product_info item = product - { name: product.name } if item + { name: product.name, product_type: product_type } if item end def state diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb index 1c573d778..dc659b2d9 100644 --- a/ruby/lib/jam_ruby/models/school.rb +++ b/ruby/lib/jam_ruby/models/school.rb @@ -18,6 +18,8 @@ module JamRuby has_many :students, class_name: ::JamRuby::User has_many :teachers, class_name: ::JamRuby::Teacher has_many :school_invitations, class_name: 'JamRuby::SchoolInvitation' + has_many :teacher_payments, class_name: 'JamRuby::TeacherPayment' + has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' validates :user, presence: true validates :enabled, inclusion: {in: [true, false]} diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index 69640f35a..05ef88c1f 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -44,10 +44,25 @@ module JamRuby SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all end - def self.most_recent_redirect(user, default) + def self.most_recent_redirect(user, default, queryParams=nil) + puts "jquery params" hint = SignupHint.where(user_id: user.id).order('created_at desc').first + if hint - hint.redirect_location + redirect = hint.redirect_location + puts "redirect #{redirect}" + uri = URI.parse(redirect) + bits = uri.query ? URI.decode_www_form(uri.query) : [] + if queryParams + queryParams.each do |k, v| + bits << [k, v] + end + end + + puts "bits #{bits}" + uri.query = URI.encode_www_form(bits) + puts "oh yeah #{uri.to_s}" + return uri.to_s else default end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index a6982e944..5135b91e4 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -55,7 +55,7 @@ module JamRuby query = User.joins(:teacher) - # only show teachers with background check set and ready for session set to true + # only show teachers with ready for session set to true query = query.where('teachers.ready_for_session_at IS NOT NULL') instruments = params[:instruments] @@ -145,7 +145,11 @@ module JamRuby def self.save_teacher(user, params) teacher = build_teacher(user, params) - teacher.save + if teacher.save + # flag the user as a teacher + teacher.user.is_a_teacher = true + teacher.user.save(validate: false) + end teacher end @@ -190,8 +194,10 @@ module JamRuby teacher.price_per_month_120_cents = params[:price_per_month_120_cents] if params.key?(:price_per_month_120_cents) teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive) teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) + teacher.test_drives_per_week ||= 10 # default to 10 in absence of others teacher.school_id = params[:school_id] if params.key?(:school_id) + # Many-to-many relations: if params.key?(:genres) genres = params[:genres] @@ -317,7 +323,7 @@ module JamRuby end def has_stripe_billing? - false + user.has_stripe_connect? end def has_instruments_or_subject? diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 585d8e61b..552a5959c 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -5,10 +5,43 @@ module JamRuby belongs_to :teacher_payment, class_name: "JamRuby::TeacherPayment" belongs_to :lesson_session, class_name: "JamRuby::LessonSession" belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" + belongs_to :school, class_name: "JamRuby::School" validates :teacher, presence: true validates :amount_in_cents, presence: true + def self.index(current_user, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = TeacherDistribution.where(teacher_id: current_user.id).where('school_id IS NULL').order('created_at desc') + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + + def not_collectable + if is_test_drive? + false + elsif is_normal? + !lesson_session.billing_should_retry + else + ! lesson_package_purchase.billing_should_retry + end + end + def self.create_for_lesson(lesson_session) distribution = create(lesson_session) distribution.lesson_session = lesson_session @@ -26,13 +59,35 @@ module JamRuby distribution.teacher = target.teacher distribution.ready = false distribution.distributed = false - distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target) + distribution.school = target.lesson_booking.school distribution end + def amount amount_in_cents / 100.0 end + def real_distribution_in_cents + amount_in_cents - calculate_teacher_fee + end + + def real_distribution + (real_distribution_in_cents / 100.0) + end + + def real_distribution_display + '$%.2f' % real_distribution + end + + def calculate_teacher_fee + if is_test_drive? + 0 + else + (amount_in_cents * (teacher.teacher.jamkazam_rate + 0.03)).round + end + end + def student if lesson_session lesson_session.student @@ -59,17 +114,9 @@ module JamRuby def description if lesson_session - if lesson_session.lesson_booking.is_test_drive? - "Test Drive session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}" - elsif lesson_session.lesson_booking.is_normal? - if lesson_session.lesson_booking.is_weekly_payment? || lesson_session.lesson_booking.is_monthly_payment? - raise "Should not be here" - else - "A session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}" - end - end + lesson_session.timed_description else - "Monthly session for the month of #{lesson_package_purchase.month_name} with #{lesson_package_purchase.lesson_booking.student.name}" + lesson_package_purchase.description end end end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 3ee7ee162..a0fa6deff 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -4,12 +4,22 @@ module JamRuby belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + belongs_to :school, class_name: "JamRuby::School" def self.hourly_check teacher_payments end + # pay the school if the payment owns the school; otherwise default to the teacher + def payable_teacher + if school + school.owner + else + teacher + end + end + def teacher_distributions [teacher_distribution] end @@ -51,15 +61,10 @@ module JamRuby 24 end - def calculate_teacher_fee - if teacher_distribution.is_test_drive? - 0 - else - (amount_in_cents * 0.28).round - end + def real_distribution_in_cents + amount_in_cents - fee_in_cents end - # will find, for a given teacher, an outstading unsuccessful payment or make a new one. # it will then associate a charge with it, and then execute the charge. def self.charge(teacher) @@ -79,12 +84,13 @@ module JamRuby payment.teacher_distribution = teacher_distribution end - + payment.school = payment.teacher_distribution.school payment.amount_in_cents = payment.teacher_distribution.amount_in_cents - payment.fee_in_cents = payment.calculate_teacher_fee + payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee if payment.teacher_payment_charge.nil? charge = TeacherPaymentCharge.new + charge.user = payment.payable_teacher charge.amount_in_cents = payment.amount_in_cents charge.fee_in_cents = payment.fee_in_cents charge.teacher_payment = payment diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb index e54bc5330..0f82e48e2 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -12,15 +12,13 @@ module JamRuby end def teacher - @teacher ||= teacher_payment.teacher + @teacher ||= teacher_payment.payable_teacher end def charged_user teacher end - - def do_charge(force) # source will let you supply a token. But... how to get a token in this case? @@ -38,11 +36,17 @@ module JamRuby end def do_send_notices - UserMailer.teacher_distribution_done(teacher_payment) + unless teacher_payment.school && distribution.is_monthly? + # we don't send monthly success notices to the teacher if they are in a school, otherwise they get an email + UserMailer.teacher_distribution_done(teacher_payment).deliver + end + if teacher_payment.school + UserMailer.school_distribution_done(teacher_payment).deliver + end end def do_send_unable_charge - UserMailer.teacher_distribution_fail(teacher_payment) + UserMailer.teacher_distribution_fail(teacher_payment).deliver end def construct_description diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 3e187b491..98120a686 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -51,7 +51,7 @@ module JamRuby has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" has_many :reviews, :class_name => "JamRuby::Review" - has_one :review_summary, :class_name => "JamRuby::ReviewSummary" + has_one :review_summary, :class_name => "JamRuby::ReviewSummary" # calendars (for scheduling NOT in music_session) has_many :calendars, :class_name => "JamRuby::Calendar" @@ -71,7 +71,7 @@ module JamRuby has_many :band_musicians, :class_name => "JamRuby::BandMusician" has_many :bands, :through => :band_musicians, :class_name => "JamRuby::Band" has_one :teacher, :class_name => "JamRuby::Teacher" - + # genres has_many :genre_players, as: :player, class_name: "JamRuby::GenrePlayer", dependent: :destroy has_many :genres, through: :genre_players, class_name: "JamRuby::Genre" @@ -101,7 +101,7 @@ module JamRuby has_many :followers, :as => :followable, :class_name => "JamRuby::Follow", :dependent => :destroy # text messages - has_many :text_messages, :class_name => "JamRuby:TextMessage", :foreign_key => "target_user_id" + has_many :text_messages, :class_name => "JamRuby::TextMessage", :foreign_key => "target_user_id" # notifications has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id" @@ -176,6 +176,7 @@ module JamRuby has_many :teacher_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "teacher_id", inverse_of: :teacher has_many :teacher_distributions, :class_name => "JamRuby::TeacherDistribution", :foreign_key => "teacher_id", inverse_of: :teacher has_many :teacher_payments, :class_name => "JamRuby::TeacherPayment", :foreign_key => "teacher_id", inverse_of: :teacher + belongs_to :desired_package, :class_name => "JamRuby::LessonPackageType", :foreign_key => "lesson_package_type_id", inverse_of: :user_desired_packages # used to hold whether user last wanted test drive 4/2/1 # Shopping carts has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" @@ -209,6 +210,8 @@ module JamRuby before_save :create_remember_token, :if => :should_validate_password? before_save :stringify_avatar_info, :if => :updating_avatar + after_save :after_save + validates :first_name, length: {maximum: 50}, no_profanity: true validates :last_name, length: {maximum: 50}, no_profanity: true validates :biography, length: {maximum: 4000}, no_profanity: true @@ -255,11 +258,22 @@ module JamRuby scope :musicians_geocoded, musicians.geocoded_users scope :email_opt_in, where(:subscribe_email => true) + def after_save + if school_interest && !school_interest_was + AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/schools page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/school_interests", subject: "#{email} is interested in schools"}).deliver + if owned_school.nil? + school = School.new + school.user = self + school.save! + end + end + end def update_teacher_pct if teacher teacher.update_profile_pct end end + def user_progression_fields @user_progression_fields ||= Set.new ["first_downloaded_client_at", "first_ran_client_at", "first_music_session_at", "first_real_music_session_at", "first_good_music_session_at", "first_certified_gear_at", "first_invited_at", "first_friended_at", "first_recording_at", "first_social_promoted_at", "first_played_jamtrack_at"] end @@ -1090,6 +1104,7 @@ module JamRuby teacher = options[:teacher] school_invitation_code = options[:school_invitation_code] school_id = options[:school_id] + school_interest = options[:school_interest] user = User.new user.validate_instruments = true @@ -1115,6 +1130,7 @@ module JamRuby user.has_redeemable_jamtrack = true user.is_a_student = !!student user.is_a_teacher = !!teacher + user.school_interest = !!school_interest if user.is_a_student || user.is_a_teacher musician = true end @@ -1125,7 +1141,7 @@ module JamRuby user.school_id = school_id elsif user.is_a_teacher school = School.find_by_id(school_id) - school_name = school ? school.name : 'a music school' + school_name = school ? school.name : 'a music school' user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Teaches for #{school_name}", school_id: school_id) end else @@ -1318,16 +1334,13 @@ module JamRuby end if affiliate_referral_id.present? - if user.is_a_student UserMailer.student_welcome_message(user).deliver - end - - if user.is_a_teacher + elsif user.is_a_teacher UserMailer.teacher_welcome_message(user).deliver - end - - if !user.is_a_teacher && !user.is_a_student + elsif user.school_interest + UserMailer.school_owner_welcome_message(user).deliver + else UserMailer.welcome_message(user).deliver end @@ -1891,7 +1904,7 @@ module JamRuby end def can_buy_test_drive? - lesson_purchases.where(lesson_package_type_id: LessonPackageType.test_drive.id).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 + lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 end def has_test_drives? @@ -1906,6 +1919,15 @@ module JamRuby !requested_test_drive(teacher).nil? end + def stripe_auth + user_authorizations.where(provider: "stripe_connect").first + end + + def has_stripe_connect? + auth = stripe_auth + auth && (!auth.token_expiration || auth.token_expiration > Time.now) + end + def fetch_stripe_customer Stripe::Customer.retrieve(stripe_customer_id) end @@ -1931,9 +1953,11 @@ module JamRuby customer end - def card_approved(token, zip) + + def card_approved(token, zip, booking_id) approved_booking = nil + found_uncollectables = nil User.transaction do self.stripe_token = token if token self.stripe_zip_code = zip if zip @@ -1941,14 +1965,22 @@ module JamRuby self.stripe_customer_id = customer.id self.stored_credit_card = true if self.save - # we can also 'unlock' any booked sessions that still need to be done so - LessonBooking.unprocessed(self).each do |booking| - booking.card_approved - approved_booking = booking + if booking_id + approved_booking = LessonBooking.find_by_id(booking_id) + if approved_booking + approved_booking.card_approved + end + end + + if uncollectables.count > 0 + found_uncollectables = uncollectables + uncollectables.update_all(billing_should_retry: true) + else + found_uncollectables = nil end end end - approved_booking + [approved_booking, found_uncollectables] end def update_name(name) @@ -1977,6 +2009,8 @@ module JamRuby normal = nil intent = nil purchase = nil + lesson_package_type = nil + uncollectables = nil User.transaction do if params[:name].present? @@ -1985,20 +2019,32 @@ module JamRuby end end - booking = card_approved(params[:token], params[:zip]) + booking, uncollectables = card_approved(params[:token], params[:zip], params[:booking_id]) if params[:test_drive] self.reload - result = Sale.purchase_test_drive(self, booking) + if booking + lesson_package_type = booking.resolved_test_drive_package + end + if lesson_package_type.nil? + lesson_package_type = LessonPackageType.test_drive_4 + end + + + result = Sale.purchase_test_drive(self, lesson_package_type, booking) test_drive = result[:sale] purchase = result[:purchase] + + if booking && !purchase.errors.any? + # the booking would not have a lesson_package_purchase associated yet, so let's associate it + booking.lesson_sessions.update_all(lesson_package_purchase_id: purchase.id) + end elsif params[:normal] self.reload end - intent = TeacherIntent.recent_test_drive(self) end - {lesson: booking, test_drive: test_drive, intent:intent, purchase: purchase} + {lesson: booking, test_drive: test_drive, purchase: purchase, lesson_package_type: lesson_package_type, uncollectables: uncollectables} end def requested_test_drive(teacher = nil) @@ -2018,7 +2064,16 @@ module JamRuby end def most_recent_test_drive_purchase - lesson_purchases.where(lesson_package_type_id: LessonPackageType.test_drive.id).order('created_at desc').first + lesson_purchases.where('lesson_package_type_id in (?)', [LessonPackageType.test_drive_package_ids]).order('created_at desc').first + end + + def total_test_drives + purchase = most_recent_test_drive_purchase + if purchase + purchase.test_drive_count + else + 0 + end end def test_drive_succeeded(lesson_session) @@ -2031,16 +2086,26 @@ module JamRuby end end + def test_drive_declined(lesson_session) + # because we decrement test_drive credits as soon as you book, we need to bring it back now + self.remaining_test_drives = self.remaining_test_drives + 1 + self.save(validate: false) + end + def test_drive_failed(lesson_session) # because we decrement test_drive credits as soon as you book, we need to bring it back now self.remaining_test_drives = self.remaining_test_drives + 1 self.save(validate: false) - UserMailer.test_drive_no_bill(self, lesson_session).deliver + UserMailer.student_test_drive_no_bill(lesson_session).deliver end def used_test_drives - 4 - remaining_test_drives + total_test_drives - remaining_test_drives + end + + def uncollectables(limit = 10) + LessonPaymentCharge.where(user_id:self.id).order(:created_at).where('billing_attempts > 0').where(billed: false).limit(limit) end def has_rated_teacher(teacher) diff --git a/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb new file mode 100644 index 000000000..ad9df11a3 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb @@ -0,0 +1,16 @@ +module JamRuby + class MinutelyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_minutely_job + @@log = Logging.logger[MinutelyJob] + + def self.perform + @@log.debug("waking up") + + LessonSession.minutely_check + + @@log.debug("done") + end + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 73c0a9fad..f199e041b 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -980,7 +980,7 @@ FactoryGirl.define do price 30.00 factory :test_drive_purchase do - lesson_package_type { JamRuby::LessonPackageType.test_drive } + lesson_package_type { JamRuby::LessonPackageType.test_drive_4 } association :lesson_booking, factory: :lesson_booking price 49.99 end @@ -1010,6 +1010,13 @@ FactoryGirl.define do factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do type 'JamRuby::TeacherPaymentCharge' + association :user, factory: :user + end + + + factory :lesson_payment_charge, parent: :charge, class: 'JamRuby::LessonPaymentCharge' do + type 'JamRuby::LessonPaymentCharge' + association :user, factory: :user end 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 8dc2eb3e6..8b0292922 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -38,11 +38,11 @@ describe "Monthly Recurring Lesson Flow" do ########## Need validate their credit card token = create_stripe_token result = user.payment_update({token: token, zip: '78759', normal: true}) - booking = result[:lesson] - lesson = booking.lesson_sessions[0] + booking.card_presumed_ok.should be_true booking.errors.any?.should be_false lesson.errors.any?.should be_false - booking.card_presumed_ok.should be_true + booking = result[:lesson] + lesson = booking.lesson_sessions[0] booking.sent_notices.should be_true lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) lesson.amount_charged.should be 0.0 @@ -116,7 +116,7 @@ describe "Monthly Recurring Lesson Flow" do # puts del.inspect end # get acceptance emails, as well as 'your stuff is accepted' - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 lesson_session.errors.any?.should be_false lesson_session.reload lesson_session.slot.should eql student_counter @@ -126,9 +126,10 @@ describe "Monthly Recurring Lesson Flow" do 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 6 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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 @@ -185,7 +186,7 @@ describe "Monthly Recurring Lesson Flow" do lesson_purchase.sale_line_item.should eql line_item TeacherPayment.count.should eql 0 - TeacherPayment.daily_check + TeacherPayment.hourly_check teacher_distribution.reload teacher_distribution.distributed.should be_true TeacherPayment.count.should eql 1 diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb index fa408e64d..404978968 100644 --- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -12,9 +12,12 @@ describe "Normal Lesson Flow" do let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:school) {FactoryGirl.create(:school)} describe "stripe mocked" do - before { StripeMock.start + before { + StripeMock.clear_errors + StripeMock.start teacher.stripe_account_id = stripe_account1_id teacher.save! } @@ -35,7 +38,7 @@ describe "Normal Lesson Flow" do ########## Need validate their credit card token = create_stripe_token - result = user.payment_update({token: token, zip: '78759', normal: true}) + 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 @@ -71,9 +74,10 @@ describe "Normal Lesson Flow" do 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 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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 @@ -91,6 +95,7 @@ describe "Normal Lesson Flow" do 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 @@ -177,12 +182,12 @@ describe "Normal Lesson Flow" do lesson_session.reload payment = lesson_session.lesson_payment_charge - payment.amount_in_cents.should eql 3248 payment.fee_in_cents.should eql 0 + lesson_session.billing_attempts.should eql 4 lesson_session.post_processed.should be_true - LessonPaymentCharge.count.should eql 2 + LessonPaymentCharge.count.should eql 1 lesson_session.reload lesson_session.analysed.should be_true @@ -266,7 +271,7 @@ describe "Normal Lesson Flow" do ########## Need validate their credit card token = create_stripe_token - result = user.payment_update({token: token, zip: '78759', normal: true}) + 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 @@ -351,9 +356,10 @@ describe "Normal Lesson Flow" do 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 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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 @@ -371,6 +377,7 @@ describe "Normal Lesson Flow" do 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 @@ -411,4 +418,167 @@ describe "Normal Lesson Flow" do user.remaining_test_drives.should eql 0 UserMailer.deliveries.length.should eql 2 # one for student, one for teacher end + + + it "works (school on school)" do + + # get user and teacher into same school + user.school = school + user.save! + teacher.school = school + 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.school.should be_true + booking.card_presumed_ok.should be_false + booking.user.should eql user + user.unprocessed_normal_lesson.should eql [] + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + booking.is_requested?.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + LessonPaymentCharge.count.should eql 0 + + 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}) + 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 = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + 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.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + 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 + + LessonPaymentCharge.count.should eql 0 + TeacherDistribution.count.should eql 0 + end end diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb index 3946eecdc..8eba1de70 100644 --- a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb @@ -13,6 +13,9 @@ describe "Recurring Lesson Flow" do let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + before(:each) do + Timecop.return + end it "works" do # user has no test drives, no credit card on file, but attempts to book a lesson @@ -27,7 +30,7 @@ describe "Recurring Lesson Flow" do ########## Need validate their credit card token = create_stripe_token - result = user.payment_update({token: token, zip: '78759', normal: true}) + 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 @@ -107,7 +110,7 @@ describe "Recurring Lesson Flow" do # puts del.inspect end # get acceptance emails, as well as 'your stuff is accepted' - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 lesson_session.errors.any?.should be_false lesson_session.reload lesson_session.slot.should eql student_counter @@ -117,9 +120,10 @@ describe "Recurring Lesson Flow" do 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 6 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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 @@ -138,6 +142,7 @@ describe "Recurring Lesson Flow" do 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 diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb index e9545cfbe..6cd7c13b0 100644 --- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -14,12 +14,16 @@ describe "TestDrive Lesson Flow" do let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } before { + Timecop.return teacher.stripe_account_id = stripe_account1_id teacher.save! } it "works" do + user.desired_package = LessonPackageType.test_drive_2 + user.save! + # user has no test drives, no credit card on file, but attempts to book a lesson booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") booking.errors.any?.should be_false @@ -28,20 +32,24 @@ describe "TestDrive Lesson Flow" do booking.card_presumed_ok.should be_false booking.should eql user.unprocessed_test_drive booking.sent_notices.should be_false + user.reload user.remaining_test_drives.should eql 0 ########## Need validate their credit card token = create_stripe_token - result = user.payment_update({token: token, zip: '78759', test_drive: true}) - user.reload - user.remaining_test_drives.should eql 3 + result = user.payment_update({token: token, zip: '78759', test_drive: true, booking_id: booking.id}) booking = result[:lesson] - lesson = booking.lesson_sessions[0] - test_drive = result[:test_drive] booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + lesson.errors.any?.should be_false + test_drive = result[:test_drive] test_drive.errors.any?.should be_false + + user.reload + user.remaining_test_drives.should eql 1 + 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) @@ -49,20 +57,20 @@ describe "TestDrive Lesson Flow" do test_drive.stripe_charge_id.should_not be_nil - test_drive.recurly_tax_in_cents.should be 412 - test_drive.recurly_total_in_cents.should eql 4999 + 412 - test_drive.recurly_subtotal_in_cents.should eql 4999 + test_drive.recurly_tax_in_cents.should be 247 + test_drive.recurly_total_in_cents.should eql 2999 + 247 + test_drive.recurly_subtotal_in_cents.should eql 2999 test_drive.recurly_currency.should eql 'USD' line_item = test_drive.sale_line_items[0] line_item.quantity.should eql 1 line_item.product_type.should eql SaleLineItem::LESSON - line_item.product_id.should eq LessonPackageType.test_drive.id + line_item.product_id.should eq LessonPackageType.test_drive_2.id user.reload user.stripe_customer_id.should_not be nil user.lesson_purchases.length.should eql 1 - user.remaining_test_drives.should eql 3 + user.remaining_test_drives.should eql 1 lesson_purchase = user.lesson_purchases[0] - lesson_purchase.price.should eql 49.99 + lesson_purchase.price.should eql 29.99 lesson_purchase.lesson_package_type.is_test_drive?.should eql true customer = Stripe::Customer.retrieve(user.stripe_customer_id) @@ -134,7 +142,7 @@ describe "TestDrive Lesson Flow" do 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 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last chat.message.should eql 'Yeah I got this' chat.channel.should eql ChatMessage::CHANNEL_LESSON @@ -145,7 +153,7 @@ describe "TestDrive Lesson Flow" do notification.student_directed.should eql true notification.purpose.should eql 'accept' notification.description.should eql NotificationTypes::LESSON_MESSAGE - notification.message.should eql "Your lesson request is confirmed!" + notification.message.should be_nil # teacher & student get into session @@ -156,6 +164,7 @@ describe "TestDrive Lesson Flow" do lesson_session.music_session.session_removed_at = end_time lesson_session.music_session.save! + Timecop.travel(end_time + 1) UserMailer.deliveries.clear @@ -173,11 +182,11 @@ describe "TestDrive Lesson Flow" do lesson_session.billing_error_reason.should be_nil lesson_session.sent_notices.should be true purchase = lesson_session.lesson_package_purchase - purchase.should_not be nil - purchase.price_in_cents.should eql 4999 + purchase.should_not be_nil + purchase.price_in_cents.should eql 2999 purchase.lesson_package_type.is_test_drive?.should be true user.reload - user.remaining_test_drives.should eql 3 + user.remaining_test_drives.should eql 1 UserMailer.deliveries.length.should eql 2 # one for student, one for teacher teacher_distribution = lesson_session.teacher_distribution @@ -194,7 +203,7 @@ describe "TestDrive Lesson Flow" do teacher_distribution.distributed.should be_false TeacherPayment.count.should eql 0 - TeacherPayment.daily_check + TeacherPayment.hourly_check TeacherPayment.count.should eql 1 lesson_session.reload diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 99fd536a5..d5f080cfc 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -783,6 +783,22 @@ describe AffiliatePartner do end end + describe "edge case" do + it "year change" do + partner.touch + last_day_of_year = Date.new(2015, 12, 31) + first_day_of_next_year = Date.new(2016, 01, 01) + AffiliatePartner.tally_up(last_day_of_year) + AffiliatePartner.tally_up(first_day_of_next_year) + quarterly_payment = AffiliateQuarterlyPayment.where(year: 2016, quarter: 0, affiliate_partner_id: partner.id).first + quarterly_payment.closed.should be_false + AffiliatePartner.tally_up(Date.new(2016, 01, 02)) + quarterly_payment.reload + quarterly_payment.closed.should be_false + end + end + + describe "boundary_dates_for_month" do it "invalid month" do expect{AffiliatePartner.boundary_dates_for_month(2015, 0)}.to raise_error diff --git a/ruby/spec/jam_ruby/models/language_spec.rb b/ruby/spec/jam_ruby/models/language_spec.rb new file mode 100644 index 000000000..ae1961cf1 --- /dev/null +++ b/ruby/spec/jam_ruby/models/language_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Language do + + it "english_sort" do + sorted= Language.english_sort + sorted[0].id.should eql 'EN' + sorted[1].id.should eql 'AF' + sorted[-1].id.should eql 'XH' + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb index ac64a0f3a..29a5f2cec 100644 --- a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -112,7 +112,7 @@ describe LessonBooking do purchase.last_billing_attempt_at.should eql time purchase.sent_billing_notices.should eql false - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', booking.id) user.save! day = day + 1 @@ -137,7 +137,7 @@ describe LessonBooking do end it "advances to next month" do - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', nil) user.save! day = Date.new(2016, 1, 20) @@ -258,7 +258,7 @@ describe LessonBooking do purchase.billed.should be false # now that it's suspended, let's unsuspend it - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', booking.id) user.save! day = day + 1 @@ -386,38 +386,6 @@ describe LessonBooking do end describe "book_free" do - it "works" do - - pending "free not supported" - booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") - booking.errors.any?.should be false - booking.user.should eq user - booking.teacher.should eq teacher_user - booking.description.should eq ("Hey I've heard of you before.") - booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_ELSEWHERE - booking.recurring.should eq false - booking.lesson_length.should eq 30 - booking.lesson_type.should eq LessonBooking::LESSON_TYPE_FREE - booking.lesson_booking_slots.length.should eq 2 - - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first - chat_message.should_not be_nil - chat_message.message.should eq booking.description - - user.reload - user.remaining_free_lessons.should eq 0 - user.remaining_test_drives.should eq 1 - - booking.card_presumed_ok.should eq false - booking.sent_notices.should eq false - - user.card_approved(create_stripe_token, '78759') - user.save! - booking.reload - booking.sent_notices.should eq true - booking.card_presumed_ok.should eq true - - end it "allows long message to flow through chat" do @@ -426,7 +394,7 @@ describe LessonBooking do booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -446,16 +414,6 @@ describe LessonBooking do ChatMessage.count.should eq 1 end - it "prevents user without stored credit card" do - - pending "free not supported" - user.stored_credit_card = false - user.save! - - booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") - booking.errors.any?.should be false - end - it "must have 2 lesson booking slots" do booking = LessonBooking.book_test_drive(user, teacher_user, [], "Hey I've heard of you before.") @@ -488,7 +446,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_TEST_DRIVE booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -502,7 +460,7 @@ describe LessonBooking do booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -520,7 +478,7 @@ describe LessonBooking do booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") booking.errors.any?.should be true - booking.errors[:user].should eq ["has a requested TestDrive with this teacher"] + booking.errors[:user].should eq ["have a requested TestDrive with this teacher"] ChatMessage.count.should eq 1 end @@ -549,7 +507,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -570,7 +528,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -579,12 +537,12 @@ describe LessonBooking do user.remaining_test_drives.should eq 1 end - it "allows long message to flow through chat" do + it "allows long message to flow through chat" do booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, Faker::Lorem.characters(10000), true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -709,6 +667,7 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: teacher_user, message: 'meh', slot: booking.default_slot.id, update_all: false}) lesson_session.errors.any?.should be_false + lesson_session.reload lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload booking.reload @@ -732,8 +691,8 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) lesson_session.errors.any?.should be_false - lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED booking.reload booking.status.should eql LessonSession::STATUS_CANCELED UserMailer.deliveries.length.should eql 2 @@ -784,8 +743,8 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: true}) lesson_session.errors.any?.should be_false - lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED booking.reload booking.status.should eql LessonSession::STATUS_CANCELED booking.canceler.should eql user diff --git a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb index fcd2816b5..29fe366a1 100644 --- a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb @@ -13,6 +13,32 @@ describe LessonSessionAnalyser do let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } describe "analyse" do + + after{ + Timecop.return + } + it "neither show" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + Timecop.freeze((lesson.music_session.scheduled_start + lesson.duration * 60) + 1) + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::TEACHER_FAULT + analysis[:student].should eql nil + analysis[:bill].should be false + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be false + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + it "teacher joined on time, waited, student no show" do lesson = testdrive_lesson(user, teacher) music_session = lesson.music_session @@ -21,8 +47,7 @@ describe LessonSessionAnalyser do start = lesson.scheduled_start end_time = lesson.scheduled_start + (60 * lesson.duration) uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) - lesson.music_session.session_removed_at = end_time - lesson.music_session.save! + Timecop.travel(end_time + 1) analysis = LessonSessionAnalyser.analyse(lesson) @@ -41,6 +66,36 @@ describe LessonSessionAnalyser do together[:session_time].should eql 0 end + it "student joined 1 min before start time, is waiting for 12 minutes" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: start - 60, session_removed_at: nil) + + Timecop.freeze(start + 11 * 60) + + analysis = LessonSessionAnalyser.analyse(lesson, true) + analysis[:reason].should eql LessonSessionAnalyser::TEACHER_FAULT + analysis[:teacher].should eql LessonSessionAnalyser::NO_SHOW + analysis[:student].should be_nil + analysis[:bill].should be false + + student = analysis[:student_analysis] + student[:joined_on_time].should be true + student[:joined_late].should be false + student[:waited_correctly].should be true + student[:initial_waiting_pct].should eql 1.0 + student[:potential_waiting_time].should eql 600.0 + + student[:session_time].should eql (11 * 60).to_f + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + it "teacher joined on time, waited, student joined late" do lesson = testdrive_lesson(user, teacher) music_session = lesson.music_session @@ -52,12 +107,10 @@ describe LessonSessionAnalyser do uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: late_start, session_removed_at: late_start + 4 * 60) uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) - lesson.music_session.session_removed_at = end_time - lesson.music_session.save! + Timecop.travel(end_time + 1) analysis = LessonSessionAnalyser.analyse(lesson) - puts analysis analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT analysis[:student].should eql LessonSessionAnalyser::JOINED_LATE analysis[:bill].should be true @@ -92,8 +145,7 @@ describe LessonSessionAnalyser do analysis = LessonSessionAnalyser.analyse(lesson) analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING - lesson.music_session.session_removed_at = end_time - lesson.music_session.save! + Timecop.travel(end_time + 1) analysis = LessonSessionAnalyser.analyse(lesson) analysis[:reason].should eql LessonSessionAnalyser::SUCCESS diff --git a/ruby/spec/jam_ruby/models/lesson_session_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_spec.rb index e1470bdff..938de25e7 100644 --- a/ruby/spec/jam_ruby/models/lesson_session_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' describe LessonSession do - let(:user) {FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1)} + let(:user) {FactoryGirl.create(:user, stored_credit_card: true, remaining_free_lessons: 1, remaining_test_drives: 1)} let(:teacher) {FactoryGirl.create(:teacher_user)} let(:slot1) { FactoryGirl.build(:lesson_booking_slot_single) } let(:slot2) { FactoryGirl.build(:lesson_booking_slot_single) } - let(:lesson_booking) {LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)} + let(:lesson_booking) {b = LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60); b.card_presumed_ok = true; b.save!; b} let(:lesson_session) {lesson_booking.lesson_sessions[0]} describe "accept" do @@ -16,10 +16,28 @@ describe LessonSession do end end + describe "upcoming_sessions_reminder" do + it "succeeds" do + UserMailer.deliveries.clear + LessonSession.upcoming_sessions_reminder + lesson_session.touch + lesson_session.sent_starting_notice.should be_false + lesson_session.is_requested?.should be_true + lesson_session.music_session.scheduled_start = 15.minutes.from_now + lesson_session.music_session.save! + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 2 + UserMailer.deliveries.clear + lesson_session.reload + lesson_session.sent_starting_notice.should be_true + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 0 + end + end + describe "index" do it "finds single lesson as student" do - lesson_booking.touch lesson_session.touch lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user lesson_session.lesson_booking.teacher.should eql teacher diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index e152fed55..4013eb804 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -362,6 +362,31 @@ describe MusicSession do end describe "scheduled" do + + it "includes any RSVP'ed" do + rsvp_request = FactoryGirl.create(:rsvp_request_for_multiple_slots, user: some_user, music_session: music_session1, number: 2, chosen:true) + + approved_rsvps = music_session1.approved_rsvps + approved_rsvps.length.should == 2 + + + sessions = MusicSession.scheduled(approved_rsvps[0]) + sessions.length.should == 1 + + sessions = MusicSession.scheduled(approved_rsvps[1]) + sessions.length.should == 1 + end + + it "includes invited" do + invitee = FactoryGirl.create(:user, last_jam_audio_latency: 30, last_jam_locidispid: 3) + FactoryGirl.create(:friendship, user: creator, friend: invitee) + FactoryGirl.create(:friendship, user: invitee, friend: creator) + music_session = FactoryGirl.create(:music_session, creator: creator) + FactoryGirl.create(:invitation, receiver:invitee, sender:creator, music_session: music_session) + + sessions = MusicSession.scheduled(invitee) + sessions.length.should == 1 + end it "excludes based on time-range" do session = FactoryGirl.create(:music_session, scheduled_start: Time.now) diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index bc278b7e2..20291922f 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -9,6 +9,9 @@ describe Sale do let(:jam_track3) { FactoryGirl.create(:jam_track) } let(:gift_card) { GiftCardType.jam_track_5 } + before(:each) { + Timecop.return + } def assert_free_line_item(sale_line_item, jamtrack) sale_line_item.recurly_tax_in_cents.should be_nil sale_line_item.recurly_total_in_cents.should be_nil @@ -596,6 +599,8 @@ describe Sale do lesson_session.music_session.session_removed_at = end_time lesson_session.music_session.save! + Timecop.travel(end_time + 1) + # bill the user LessonSession.hourly_check @@ -649,6 +654,8 @@ describe Sale do lesson_session.music_session.session_removed_at = end_time lesson_session.music_session.save! + Timecop.travel(end_time + 1) + # bill the user LessonSession.hourly_check @@ -715,7 +722,7 @@ describe Sale do booking.should eql user.unprocessed_test_drive token = create_stripe_token - result = user.payment_update({token: token, zip: '72205', test_drive: true}) + result = user.payment_update({token: token, zip: '72205', test_drive: true, booking_id: booking.id}) booking.reload booking.card_presumed_ok.should be_true @@ -731,7 +738,7 @@ describe Sale do line_item = sale.sale_line_items[0] line_item.quantity.should eql 1 line_item.product_type.should eql SaleLineItem::LESSON - line_item.product_id.should eq LessonPackageType.test_drive.id + line_item.product_id.should eq LessonPackageType.test_drive_4.id user.reload user.stripe_customer_id.should_not be nil @@ -762,7 +769,7 @@ describe Sale do user.remaining_test_drives.should eql 0 token = create_stripe_token - result = user.payment_update({token: token, zip: '78759', test_drive: true}) + result = user.payment_update({token: token, zip: '78759', test_drive: true, booking_id: booking.id}) booking.reload booking.card_presumed_ok.should be_true @@ -778,7 +785,7 @@ describe Sale do line_item = sale.sale_line_items[0] line_item.quantity.should eql 1 line_item.product_type.should eql SaleLineItem::LESSON - line_item.product_id.should eq LessonPackageType.test_drive.id + line_item.product_id.should eq LessonPackageType.test_drive_4.id user.reload @@ -819,7 +826,7 @@ describe Sale do line_item = sale.sale_line_items[0] line_item.quantity.should eql 1 line_item.product_type.should eql SaleLineItem::LESSON - line_item.product_id.should eq LessonPackageType.test_drive.id + line_item.product_id.should eq LessonPackageType.test_drive_4.id end it "will reject second test drive purchase" do diff --git a/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb new file mode 100644 index 000000000..dbf54d562 --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe TeacherDistribution do + + let(:teacher) {FactoryGirl.create(:teacher_user)} + + + describe "index" do + it "empty" do + TeacherDistribution.index(teacher, {})[:query].count.should eql 0 + end + + it "returns single" do + distribution = FactoryGirl.create(:teacher_distribution, teacher: teacher) + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + + distribution = FactoryGirl.create(:teacher_distribution) # some random teacher + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + end + end +end diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb index 58ac4d16f..bd85935dc 100644 --- a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -6,14 +6,17 @@ describe TeacherPayment do let(:user2) { FactoryGirl.create(:user) } let(:teacher_obj) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account1_id)} let(:teacher_obj2) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account2_id)} + let(:school_owner_teacher) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account2_id)} let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) } let(:teacher2) { FactoryGirl.create(:user, teacher: teacher_obj2) } + let(:school_teacher) { FactoryGirl.create(:user, teacher: school_owner_teacher)} let(:test_drive_lesson) {testdrive_lesson(user, teacher)} let(:test_drive_lesson2) {testdrive_lesson(user2, teacher2)} let(:test_drive_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson, teacher: teacher, teacher_payment: nil, ready:false)} let(:test_drive_distribution2) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson2, teacher: teacher2, teacher_payment: nil, ready:false)} let(:normal_lesson_session) {normal_lesson(user, teacher)} let(:normal_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: normal_lesson_session, teacher: teacher, teacher_payment: nil, ready:false)} + let(:school) {FactoryGirl.create(:school, user: school_teacher)} describe "pending_teacher_payments" do @@ -35,6 +38,21 @@ describe TeacherPayment do payments[0]['id'].should eql teacher.id end + it "school distribution" do + test_drive_distribution.school = school + test_drive_distribution.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 1 + payments[0]['id'].should eql teacher.id + end + it "multiple teachers" do test_drive_distribution.touch test_drive_distribution2.touch @@ -95,6 +113,7 @@ describe TeacherPayment do normal_distribution.ready = true normal_distribution.save! + UserMailer.deliveries.clear TeacherPayment.teacher_payments @@ -108,9 +127,13 @@ describe TeacherPayment do puts payment.teacher_payment_charge.billing_error_reason puts payment.teacher_payment_charge.billing_error_detail end + + # only one confirm email to teacher + UserMailer.deliveries.length.should eql 1 payment.teacher_payment_charge.billed.should eql true payment.teacher_payment_charge.amount_in_cents.should eql 1000 payment.teacher_payment_charge.fee_in_cents.should eql 280 + payment.teacher_payment_charge.teacher.should eql teacher 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) @@ -118,6 +141,39 @@ describe TeacherPayment do charge.application_fee.should include("fee_") end + it "charges school" do + normal_distribution.school = school + normal_distribution.ready = true + normal_distribution.save! + + UserMailer.deliveries.clear + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + normal_distribution.teacher_payment.school.should eql school + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + # one to school owner, one to teacher + UserMailer.deliveries.length.should eql 2 + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + payment.teacher_payment_charge.user.should eql school.owner + 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 eql school.owner.teacher.stripe_account_id + charge.amount.should eql 1000 + charge.application_fee.should include("fee_") + end + it "charges multiple" do test_drive_distribution.touch test_drive_distribution.ready = true @@ -240,6 +296,76 @@ describe TeacherPayment do charge.amount.should eql 1000 end + it "failed payment, then success (school)" do + StripeMock.prepare_card_error(:card_declined) + + normal_distribution.school = school + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billing_error_reason.should eql("card_declined") + payment.teacher_payment_charge.billing_error_detail.should include("declined") + + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + payment.teacher_payment_charge.stripe_charge_id.should be_nil + + StripeMock.clear_errors + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + TeacherPayment.teacher_payments + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.reload + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + 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.amount.should eql 1000 + end + it "charges multiple (with initial failure)" do StripeMock.prepare_card_error(:card_declined) diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 36e3cf949..d55ff6956 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -842,6 +842,45 @@ describe User do end end + describe "uncollectables" do + let(:user) {FactoryGirl.create(:user)} + let(:teacher) {FactoryGirl.create(:teacher_user)} + + + it "empty" do + user.uncollectables.count.should eql 0 + end + + it "one" do + lesson_session = normal_lesson(user, teacher) + lesson_session.lesson_payment_charge.user.should eql user + lesson_session.lesson_payment_charge.billing_attempts = 1 + lesson_session.lesson_payment_charge.save! + uncollectables = user.uncollectables + uncollectables.count.should eql 1 + uncollectable = uncollectables[0] + uncollectable.description.should_not be_nil + uncollectable.expected_price_in_cents.should eql 3000 + uncollectable.is_card_declined?.should be_false + end + + it "for monthly" do + lesson_session = monthly_lesson(user, teacher) + lesson_session.booked_price.should eql 30.00 + LessonBooking.hourly_check + lesson_session.lesson_payment_charge.should be_nil + purchases=LessonPackagePurchase.where(user_id: user.id) + purchases.count.should eql 1 + purchases[0].lesson_payment_charge.billing_attempts = 1 + purchases[0].lesson_payment_charge.save! + uncollectables = user.uncollectables + uncollectables.count.should eql 1 + uncollectable = uncollectables[0] + uncollectable.description.should_not be_nil + uncollectable.expected_price_in_cents.should eql 3000 + uncollectable.is_card_declined?.should be_false + end + end =begin describe "update avatar" do diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index 9fe7a4e37..7ef34fe58 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -27,6 +27,7 @@ describe "RenderMailers", :slow => true do it { @filename="welcome_message"; UserMailer.welcome_message(user).deliver } it { @filename="student_welcome_message"; UserMailer.student_welcome_message(user).deliver } + it { @filename="school_owner_welcome_message"; UserMailer.school_owner_welcome_message(user).deliver } it { @filename="confirm_email"; UserMailer.confirm_email(user, "/signup").deliver } it { @filename="password_reset"; UserMailer.password_reset(user, '/reset_password').deliver } it { @filename="password_changed"; UserMailer.password_changed(user).deliver } @@ -49,6 +50,7 @@ describe "RenderMailers", :slow => true do @filename = "teacher_welcome_message" UserMailer.teacher_welcome_message(teacher).deliver end + it "teacher_lesson_request" do @filename = "teacher_lesson_request" @@ -120,6 +122,22 @@ describe "RenderMailers", :slow => true do UserMailer.deliveries.clear UserMailer.teacher_lesson_completed(lesson).deliver end + + it "lesson_starting_soon_teacher" do + @filename = "lesson_starting_soon_teacher" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_teacher(lesson).deliver + end + + it "lesson_starting_soon_student" do + @filename = "lesson_starting_soon_student" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_student(lesson).deliver + end end end diff --git a/ruby/spec/support/lesson_session.rb b/ruby/spec/support/lesson_session.rb index 401dad8f4..291c466a5 100644 --- a/ruby/spec/support/lesson_session.rb +++ b/ruby/spec/support/lesson_session.rb @@ -33,7 +33,7 @@ def testdrive_lesson(user, teacher, slots = nil) booking.card_presumed_ok.should be_true if user.most_recent_test_drive_purchase.nil? - LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive) + LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4) end lesson.accept({message: 'Yeah I got this', slot: slots[0]}) @@ -66,7 +66,7 @@ def normal_lesson(user, teacher, slots = nil) booking.card_presumed_ok.should be_true #if user.most_recent_test_drive_purchase.nil? - # LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive) + # LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4) #end lesson.accept({message: 'Yeah I got this', slot: slots[0]}) @@ -74,6 +74,41 @@ def normal_lesson(user, teacher, slots = nil) lesson.reload lesson.slot.should eql slots[0] lesson.status.should eql LessonSession::STATUS_APPROVED + lesson.music_session.should_not be_nil + + lesson +end + + +def monthly_lesson(user, teacher, slots = nil) + + if slots.nil? + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_recurring) + slots << FactoryGirl.build(:lesson_booking_slot_recurring) + end + + if user.stored_credit_card == false + user.stored_credit_card = true + user.save! + end + + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + # puts "NORMAL BOOKING #{booking.errors.inspect}" + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + booking.card_presumed_ok.should be_true + + #if user.most_recent_test_drive_purchase.nil? + # LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4) + #end + + lesson.accept({message: 'Yeah I got this', slot: slots[0]}) + lesson.errors.any?.should be_false + lesson.reload + lesson.slot.should eql slots[0] + lesson.status.should eql LessonSession::STATUS_APPROVED + lesson.music_session.should_not be_nil lesson end \ No newline at end of file diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 21c037c4c..b32b9b348 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -7,6 +7,10 @@ def app_config 'http://localhost:3333' end + def email_partners_alias + 'partners@jamkazam.com' + end + def email_social_alias 'social@jamkazam.com' end diff --git a/web/Gemfile b/web/Gemfile index bde372771..df30e75e5 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -19,6 +19,7 @@ else end #gem 'license_finder' +gem 'pg_migrate', '0.1.14' gem 'kickbox' gem 'oj', '2.10.2' gem 'builder' @@ -89,7 +90,7 @@ gem 'htmlentities' gem 'sanitize' gem 'recurly' #gem 'guard', '2.7.3' -gem 'influxdb' #, '0.1.8' +#gem 'influxdb' #, '0.1.8' gem 'cause' # needed by influxdb gem 'influxdb-rails'# , '0.1.10' gem 'sitemap_generator' @@ -142,6 +143,7 @@ group :test, :cucumber do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' gem 'capybara', '2.4.4' + gem 'rails-assets-sinon', source: 'https://rails-assets.org' #if ENV['JAMWEB_QT5'] == '1' # # necessary on platforms such as arch linux, where pacman -S qt5-webkit is your easiet option # gem "capybara-webkit", :git => 'git://github.com/thoughtbot/capybara-webkit.git' @@ -159,6 +161,7 @@ group :test, :cucumber do # gem 'growl', '1.0.3' gem 'poltergeist' gem 'resque_spec' + gem 'timecop' #gem 'thin' end diff --git a/web/app/assets/images/content/bkg_home_jamclass.jpg b/web/app/assets/images/content/bkg_home_jamclass.jpg new file mode 100644 index 000000000..2d01fec74 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamclass.jpg differ diff --git a/web/app/assets/images/content/bkg_home_jamclass_x.jpg b/web/app/assets/images/content/bkg_home_jamclass_x.jpg new file mode 100644 index 000000000..1c78c7233 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_jamclass_x.jpg differ diff --git a/web/app/assets/images/content/icon_unread_mail.png b/web/app/assets/images/content/icon_unread_mail.png new file mode 100644 index 000000000..32f098bf5 Binary files /dev/null and b/web/app/assets/images/content/icon_unread_mail.png differ diff --git a/web/app/assets/images/down_arrow_black_pad.png b/web/app/assets/images/down_arrow_black_pad.png new file mode 100644 index 000000000..38c86597a Binary files /dev/null and b/web/app/assets/images/down_arrow_black_pad.png differ diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee index 82dbe73eb..a605afd01 100644 --- a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -40,8 +40,6 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen @noMoreSales.hide() @next = null - - refresh:() => @currentQuery = this.buildQuery() @rest.getSalesHistory(@currentQuery) diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 99b6581cf..d2c325343 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -7,6 +7,7 @@ var $document = $(document); var logger = context.JK.logger; var EVENTS = context.JK.EVENTS; + var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; var api = context.JK.Rest(); var userId; var user = {}; @@ -80,8 +81,6 @@ $('select#user_birth_date_2i', content_root).val(parseInt(birthDateMonth)); $('select#user_birth_date_3i', content_root).val(parseInt(birthDateDay)); } - - context.JK.dropdown($('select', content_root)); } function populateAccountProfileLocation(userDetail, regions, cities) { @@ -126,8 +125,6 @@ countrySelect.val(userCountry); countrySelect.attr("disabled", null) - - context.JK.dropdown(countrySelect); } @@ -169,8 +166,6 @@ countrySelect.val(userCountry); countrySelect.attr("disabled", null); - - context.JK.dropdown(countrySelect); } function populateRegions(regions, userRegion) { @@ -193,8 +188,6 @@ regionSelect.val(userRegion) regionSelect.attr("disabled", null) - - context.JK.dropdown(regionSelect); } function populateCities(cities, userCity) { @@ -217,8 +210,6 @@ citySelect.val(userCity); citySelect.attr("disabled", null); - - context.JK.dropdown(citySelect); } /****************** MAIN PORTION OF SCREEN *****************/ @@ -239,18 +230,26 @@ enableSubmits(); } + function teacherGuidance() { + + if(recentUserDetail && recentUserDetail.is_a_teacher) { + setTimeout(function() { + var $header = $('#account-edit-profile-form h2') + context.JK.HelpBubbleHelper.teacherMusicianProfile($header, $screen); + }, 2000) + } + + } function renderAccountProfile() { $.when(api.getUserProfile()) .done(function (userDetail) { recentUserDetail = userDetail; populateAccountProfile(userDetail); - + teacherGuidance(); selectLocation = new context.JK.SelectLocation(getCountryElement(), getRegionElement(), getCityElement(), app); selectLocation.load(userDetail.country, userDetail.state, userDetail.city) }); - - context.JK.dropdown($('select'), $screen); } function navToAccount() { @@ -436,7 +435,6 @@ else { cityElement.children().remove(); cityElement.append($(nilOptionStr).text(nilOptionText)); - context.JK.dropdown(cityElement); } } diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index 1f032cb5d..c058edb1f 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -64,8 +64,6 @@ $screen.find('select[name=skill_level]').val(userDetail.skill_level); $screen.find('select[name=concert_count]').val(userDetail.concert_count); $screen.find('select[name=studio_session_count]').val(userDetail.studio_session_count); - - context.JK.dropdown($('select', $screen)); } function isUserInstrument(instrument, userInstruments) { @@ -159,8 +157,6 @@ var userDetail = userDetailResponse[0]; populateAccountProfile(userDetail, instrumentsResponse[0]); }); - - context.JK.dropdown($('select'), $screen); } function navigateTo(targetLocation) { diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 927f8b9b0..cb6e454bb 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -161,7 +161,7 @@ if(options.buttons) { context._.each(options.buttons, function(button, i) { if(!button.name) throw "button.name must be specified"; - if(!button.click && !button.href) throw "button.click or button.href must be specified"; + //if(!button.click && !button.href) throw "button.click or button.href must be specified"; var buttonStyle = button.buttonStyle; if(!buttonStyle) { @@ -175,7 +175,10 @@ } else { $btn.click(function() { - button.click(); + if (button.click) { + button.click(); + } + hide(); return false; }); diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index b300cd4f2..817ab2dad 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -62,7 +62,8 @@ CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in', PREVIEW_PLAYED: 'preview_played', VST_OPERATION_SELECTED: 'vst_operation_selected', - VST_EFFECT_SELECTED: 'vst_effect_selected' + VST_EFFECT_SELECTED: 'vst_effect_selected', + LESSON_SESSION_ACTION: 'lesson_session_action' }; context.JK.PLAYBACK_MONITOR_MODE = { @@ -345,7 +346,8 @@ context.JK.NAMED_MESSAGES = { MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix', HOWTO_USE_VIDEO_NOSHOW : 'how-to-use-video', - CONFIGURE_VIDEO_NOSHOW : 'configure-video' + CONFIGURE_VIDEO_NOSHOW : 'configure-video', + TEACHER_MUSICIAN_PROFILE : 'teacher-musician-profile' } context.JK.ChannelGroupIds = { @@ -367,7 +369,9 @@ "JamTrackGroup": 15, "MetronomeGroup": 16, "MidiInputMusicGroup": 17, - "PeerMidiInputMusicGroup": 18 + "PeerMidiInputMusicGroup": 18, + "UsbInputMusicGroup": 19, + "PeerUsbInputMusicGroup": 20 }; context.JK.ChannelGroupLookup = { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index b348ed629..9c61d67eb 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -28,8 +28,7 @@ } function bigHelpOptions(options) { - return {positions: options.positions, offsetParent: options.offsetParent, - width:options.width, + var defaults = { spikeGirth: 15, spikeLength: 20, fill: 'white', @@ -40,8 +39,27 @@ fontSize: '20px', backgroundColor:'transparent', color:'#ed3618'}} + + return $.extend({}, defaults, options) } + function bigHelpDarkOptions(options) { + var defaults = { + spikeGirth: 15, + spikeLength: 20, + fill: '#242323', + cornerRadius:8, + strokeWidth: 2, + cssStyles: { + fontWeight:'bold', + fontSize: '20px', + backgroundColor:'transparent', + color:'#ed3618'}} + + return $.extend({}, defaults, options) + } + + function clearJamTrackGuideTimeout() { if(jamTrackGuideTimeout) { clearTimeout(jamTrackGuideTimeout); @@ -135,4 +153,45 @@ return context.JK.prodBubble($element, 'jamtrack-web-play', {}, bigHelpOptions({positions:['bottom'], offsetParent: $offsetParent})) } + helpBubble.teacherMusicianProfile = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'teacher-musician-profile', {}, bigHelpDarkOptions({spikeGirth:0, spikeLength: 0, duration:10000, offsetParent:$offsetParent, width:385, positions:['top', 'right', 'bottom']})) + } + + helpBubble.teacherProfile = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'teacher-profile', {}, bigHelpDarkOptions({spikeGirth:0, spikeLength: 0, duration:10000, offsetParent:$offsetParent, width:385, positions:['top', 'right', 'bottom']})) + } + + 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) { + + var $bookNow = $('a.book-now') + $bookNow.off('click').on('click', function(e) { + e.preventDefault() + callback() + return false; + }) + }}) + } + + helpBubble.showBuyTestDrive = function($element, $offsetParent, user, callback) { + return context.JK.onceBubble($element, 'side-buy-test-drive', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { + var $bookNow = $('a.book-now') + $bookNow.off('click').on('click', function(e) { + e.preventDefault() + callback() + return false; + }) + }}) + } + + helpBubble.showBuyNormalLesson = function($element, $offsetParent, user, callback) { + return context.JK.onceBubble($element, 'side-buy-normal-lesson', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { + var $bookNow = $('a.book-now') + $bookNow.off('click').on('click', function(e) { + e.preventDefault() + callback() + return false; + }) + }}) + } })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js index a96075b47..ef94e580f 100644 --- a/web/app/assets/javascripts/homeScreen.js +++ b/web/app/assets/javascripts/homeScreen.js @@ -89,6 +89,13 @@ if(gon.allow_force_native_client) { $('body').on('keyup', switchClientMode); } + + $('.homecard.jamclass').on('click', function() { + + context.JK.Banner.showNotice("Coming Soon!", "JamClass is just around the corner.") + + return false; + }) } this.initialize = function() { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 2df970203..677e0c4b5 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -478,14 +478,6 @@ function getTeacher(options) { - // var url = '/api/teacher/detail' - - // if(options && _.size(options) > 0) { - // console.log("WTF"); - // url += "?" + $.param(options) - // } - - // console.log("THE URL", url) return $.ajax({ type: "GET", dataType: "json", @@ -595,7 +587,7 @@ processData: false }); } - if(detail) { + if(detail && context.JK.currentUserId == id) { detail.done(function(user) { window.UserActions.loaded(user) }) @@ -1701,12 +1693,13 @@ }); } - function createChatMessage(options) { + function createChatMessage(data) { return $.ajax({ type: "POST", - url: '/api/chat?' + $.param(options), + url: '/api/chat', dataType: "json", - contentType: 'application/json' + contentType: 'application/json', + data: JSON.stringify(data) }); } @@ -2227,6 +2220,78 @@ }); } + function getUncollectables(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions/uncollectable", + dataType: "json", + contentType: 'application/json' + }); + } + + + function getLesson(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions/" + options.id, + dataType: "json", + contentType: 'application/json' + }); + } + + function getLessonAnalysis(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions/" + options.id + "/analysis", + dataType: "json", + contentType: 'application/json' + }); + } + + + function updateLessonSessionUnreadMessages(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/update_unread_messages", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function checkLessonReschedule(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/reschedule_check", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + + function checkLessonCancel(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/cancel_check", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function lessonStartTime(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/start_time", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } function getTestDriveStatus(options) { return $.ajax({ type: "GET", @@ -2389,6 +2454,20 @@ }); } + function listTeacherDistributions(options) { + + if(!options) { + options = {} + } + + return $.ajax({ + type: "GET", + url: "/api/teacher_distributions?" + $.param(options) , + dataType: "json", + contentType: 'application/json' + }); + } + function deleteSchoolTeacher(options) { var id = getId(options); @@ -2603,6 +2682,12 @@ this.counterLessonBooking = counterLessonBooking; this.submitStripe = submitStripe; this.getLessonSessions = getLessonSessions; + this.getUncollectables = getUncollectables; + this.getLesson = getLesson; + this.getLessonAnalysis = getLessonAnalysis; + this.updateLessonSessionUnreadMessages = updateLessonSessionUnreadMessages; + this.checkLessonCancel = checkLessonCancel; + this.checkLessonReschedule = checkLessonReschedule; this.getTestDriveStatus = getTestDriveStatus; this.createTeacherIntent = createTeacherIntent; this.getSchool = getSchool; @@ -2616,6 +2701,8 @@ this.resendSchoolInvitation = resendSchoolInvitation; this.deleteSchoolTeacher = deleteSchoolTeacher; this.deleteSchoolStudent = deleteSchoolStudent; + this.listTeacherDistributions = listTeacherDistributions; + this.lessonStartTime = lessonStartTime; return this; }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index b13b42287..161576457 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -328,6 +328,8 @@ var screen = 'home' try { var location = context.RouteMap.parse(hash); + + screen = location.page.substring(1); // remove leading slash } catch (e) { diff --git a/web/app/assets/javascripts/jquery.lessonSessionActions.js b/web/app/assets/javascripts/jquery.lessonSessionActions.js new file mode 100644 index 000000000..ba573fc95 --- /dev/null +++ b/web/app/assets/javascripts/jquery.lessonSessionActions.js @@ -0,0 +1,100 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + + // creates an iconic/graphical instrument selector. useful when there is minimal real-estate + + $.fn.lessonSessionActions = function(options) { + + return this.each(function(index) { + + function close() { + $parent.btOff(); + $parent.focus(); + } + + var $parent = $(this); + if($parent.data('lessonSessionActions')) { + // already constructed + return; + } + $parent.data('lessonSessionActions', options) + function onLessonActionSelected() { + var $li = $(this); + var lessonAction = $li.attr('data-lesson-option'); + + close(); + $parent.triggerHandler(context.JK.EVENTS.LESSON_SESSION_ACTION, {lessonAction: lessonAction, options: options}); + return false; + }; + + // if the user goes into the bubble, remove + function waitForBubbleHover($bubble) { + $bubble.hoverIntent({ + over: function() { + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + }, + out: function() { + $parent.btOff(); + }}); + } + + var timeout = null; + + var html = context._.template($('#template-lesson-session-actions').html(), options, { variable: 'data' }) + + var extraClasses = ' ' + var width = 100; + var otherOverlap = 22; + + if(options.isRequested) { + extraClasses += 'is-requested ' + width = 100; + } + else if(options.isScheduled) { + extraClasses += 'is-scheduled ' + width = 120; + } + if(options.cardNotOk) { + extraClasses += 'not-card-ok ' + width = 90; + } + if(options.isAdmin) { + extraClasses += 'is-admin ' + width = 135; + } + context.JK.hoverBubble($parent, html, { + trigger:'none', + cssClass: 'lesson-action-popup' + extraClasses, + spikeGirth:0, + spikeLength:0, + otherOverlap: ((width - 90) / 2) + 25, + overlap: -10, + width:width, + closeWhenOthersOpen: true, + offsetParent: $parent.closest('.screen'), + positions:['bottom'], + preShow: function() { + + }, + postShow:function(container) { + $(container).find('li').click(onLessonActionSelected) + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + waitForBubbleHover($(container)) + timeout = setTimeout(function() {$parent.btOff()}, 6000) + } + }); + }); + } + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 85a435521..50d9a2fe6 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -222,10 +222,10 @@ var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols, childRow, childCol, childRowspan, childColspan); - if($(this).is('.feed')) { + if($(this).is('.jamtrack')) { feedCardLayout = childLayout; } - else if($(this).is('.findsession')) { + else if($(this).is('.jamclass')) { findCardLayout = childLayout; } @@ -237,7 +237,7 @@ }, opts.animationDuration); }); - var broadcastWidth = findCardLayout.width + feedCardLayout.width; + var broadcastWidth = findCardLayout.width + feedCardLayout.width; //+ opts.gridPadding * 2; layoutBroadcast(broadcastWidth, findCardLayout.left); } @@ -634,12 +634,14 @@ if(arguments.length == 1) { // user passed in dialog id var dialogId = arguments[0]; + var result = false context._.each(openDialogs, function(dialog) { if($(dialog).attr('layout-id') == dialogId) { - return true; + result = true; + return false; } }) - return false; + return result; } else { // user passed in nothing @@ -1071,7 +1073,7 @@ } this.isDialogShowing = function() { - return isDialogShowing(); + return isDialogShowing(arguments[0]); } this.activeElementEvent = function(evtName, data) { diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index fcf3b4474..aae85ffef 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -198,6 +198,8 @@ // register text messages registerTextMessage(); + + registerLessonMessage(); } function buildParams() { @@ -230,6 +232,9 @@ if(val.description == context.JK.MessageType.TEXT_MESSAGE) { val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); } + else if(val.description == context.JK.MessageType.LESSON_MESSAGE && val.message) { + val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); + } // fill in template for Connect pre-click var template = $notificationTemplate.html(); @@ -441,7 +446,13 @@ linkSessionInfoNotification(payload, $notification, $btnNotificationAction); } else if (type === context.JK.MessageType.LESSON_MESSAGE) { - linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + if (payload.purpose == 'chat') { + linkLessonChatNotification(payload, $notification, $btnNotificationAction); + } + else + { + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + } } else if (type === context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION) { linkLessonInfoNotification(payload, $notification, $btnNotificationAction); @@ -494,6 +505,15 @@ }); } + function linkLessonChatNotification(payload, $notification, $btnNotificationAction) { + var $action_btn = $notification.find($btnNotificationAction); + $action_btn.text('LESSON CONVERSATION'); + $action_btn.click(function() { + gotoLessonChatPage({"lesson_session_id": payload.lesson_session_id}); + }); + } + + function acceptBandInvitation(args) { rest.updateBandInvitation( args.band_id, @@ -1287,6 +1307,37 @@ }); } + function registerLessonMessage() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LESSON_MESSAGE, function(header, payload) { + logger.debug("Handling LESSON_MESSAGE msg " + JSON.stringify(payload)); + + //context.ChatActions.msgReceived(payload); + + // only show if chat dialog is not showing or if the focused lesson session is not the one specified in the payload + console.log("context.ChatStore.lessonSessionId", app.layout.isDialogShowing('chat-dialog'), context.ChatStore.lessonSessionId, payload.lesson_session_id) + if (!app.layout.isDialogShowing('chat-dialog') || context.ChatStore.lessonSessionId != payload.lesson_session_id) { + app.notify({ + "title": "Lesson Message", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, [{ + id: "btn-lesson-chat", + text: "SEE LESSON CONVERSATION", + "layout-action": "close", + href: "#", + "class": "button-orange", + callback: gotoLessonChatPage, + callback_args: { + "lesson_session_id": payload.lesson_session_id + } + }] + ); + } + + handleNotification(payload, header.type); + }); + } + function registerBandInvitationAccepted() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) { logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload)); @@ -1380,6 +1431,11 @@ window.location.href = "/client#/jamclass/lesson-booking/" + args.lesson_session_id } + function gotoLessonChatPage(args) { + app.layout.showDialog('chat-dialog', {d1: "lesson_" + args.lesson_session_id}) + //window.location.href = "/client#/jamclass/chat-dialog/d1=lesson_" + args.lesson_session_id + } + function deleteNotificationHandler(evt) { evt.stopPropagation(); var notificationId = $(this).attr('notification-id'); diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 7d83a8e56..c8f7b2fb0 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -20,6 +20,7 @@ //= require ./react-components/stores/VideoStore //= require ./react-components/stores/SessionStore //= require ./react-components/stores/SessionStatsStore +//= require ./react-components/stores/BroadcastStore //= require ./react-components/stores/ChatStore //= require ./react-components/stores/MixerStore //= require ./react-components/stores/ConfigureTracksStore diff --git a/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee new file mode 100644 index 000000000..13a6d8178 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee @@ -0,0 +1,789 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountPaymentHistoryScreen = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + LIMIT: 20 + TILE_PAYMENTS_TO_YOU: 'payments to you' + TILE_PAYMENTS_TO_JAMKAZAM: 'payments to jamkazam' + TILE_PAYMENT_METHOD: 'payment method' + + STUDENT_TILES: ['payment method', 'payments to jamkazam'] + TEACHER_TILES: ['payments to jamkazam', 'payments to you'] + + onAppInit: (@app) -> + @app.bindScreen('account/paymentHistory', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + if !@shouldShowNameSet + @shouldShowNameSet = true + if userState?.user? + username = userState.user.name + first_name = userState.user.first_name + last_name = userState.user.last_name + shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1 + else + shouldShowName = @state.shouldShowName + + @setState({user: userState?.user, shouldShowName: shouldShowName}) + + + componentDidMount: () -> + @checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}] + + @root = $(@getDOMNode()) + @endOfList = @root.find('.end-of-payments-list') + @contentBodyScroller = @root + @root = $(@getDOMNode()) + @iCheckify() + + componentDidUpdate: (prevProps, prevState) -> + @iCheckify() + + $expiration = @root.find('input.expiration') + if !$expiration.data('payment-applied') + $expiration.payment('formatCardExpiry').data('payment-applied', true) + $cardNumber = @root.find("input.card-number") + if !$cardNumber.data('payment-applied') + $cardNumber.payment('formatCardNumber').data('payment-applied', true) + $cvv = @root.find("input.cvv") + if !$cvv.data('payment-applied') + $cvv.payment('formatCardCVC').data('payment-applied', true) + + if @currentNext() == null + @contentBodyScroller.off('scroll') + if @state[@getCurrentPageName()] == 1 and @getCurrentList().length == 0 + @endOfList.show() + logger.debug("PaymentHistoryScreen: empty search") + else if @state[@getCurrentPageName()] > 0 + logger.debug("end of search") + @endOfList.show() + else + @registerInfiniteScroll(@contentBodyScroller) + + if @activeTile(prevState.selected) != @activeTile() && @getCurrentList().length == 0 + @refresh() + + + registerInfiniteScroll:() -> + $scroller = @contentBodyScroller + logger.debug("registering infinite scroll") + $scroller.off('scroll') + $scroller.on('scroll', () => + # be sure to not fire off many refreshes when user hits the bottom + return if @refreshing + + if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight + #$scroller.append('
... Loading more Payments ...
') + @setState({searching: true}) + logger.debug("refreshing more payments for infinite scroll") + @nextPage() + ) + + nextPage: () -> + #nextPage = @state.salesCurrentPage + 1 + @incrementCurrentPage() + @refresh() + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({userSchedulingComm: value}) + + beforeHide: (e) -> + @screenVisible = false + @resetErrors() + + beforeShow: (e) -> + + afterShow: (e) -> + @clearResults() + @screenVisible = true + @refresh() + @getUncollectables() + + resetErrors: () -> + @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null}) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + @setState({billingInUS: checked}) + + refresh: () -> + @buildQuery() + if @activeTile() == @TILE_PAYMENTS_TO_YOU + @refreshTeacherDistributions() + else if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + @refreshSales() + else + logger.debug("dropping refresh because no tile match", @activeTile) + + refreshSales: () -> + @refreshing = true + rest.getSalesHistory(@currentQuery) + .done(@salesHistoryDone) + .fail(@salesHistoryFail) + + refreshTeacherDistributions: () -> + @refreshing = true + rest.listTeacherDistributions(@currentQuery) + .done(@teacherDistributionsDone) + .fail(@teacherDistributionsFail) + + getUncollectables: () -> + rest.getUncollectables({}) + .done(@uncollectablesDone) + .fail(@uncollectablesFail) + + salesHistoryDone:(response) -> + @refreshing = false + this.setState({salesNext: response.next, sales: this.state.sales.concat(response.entries)}) + + salesHistoryFail:(jqXHR) -> + @refreshing = false + @app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable' + + teacherDistributionsDone:(response) -> + @refreshing = false + this.setState({distributionsNext: response.next, distributions: this.state.distributions.concat(response.entries)}) + + teacherDistributionsFail:(jqXHR) -> + @refreshing = false + @app.notifyServerError jqXHR, 'Payments to You Unavailable' + + uncollectablesDone: (response) -> + this.setState({uncollectables: response}) + + uncollectablesFail: (jqXHR) -> + @app.notifyServerError jqXHR, 'Unable to fetch uncollectable info' + + clearResults:() -> + this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null, updating: false}) + + buildQuery:(page = @getCurrentPage()) -> + @currentQuery = this.defaultQuery(page) + + defaultQuery:(page = @getCurrentPage()) -> + query = + per_page: @LIMIT + page: page + 1 + if @currentNext() + query.page = @currentNext() + query + + getCurrentPage: () -> + page = this.state[@getCurrentPageName()] + if !page? + page = 1 + page + + incrementCurrentPage: () -> + newState = {} + newState[@getCurrentPageName] = @state[@getCurrentPageName()] + 1 + this.setState(newState) + + getCurrentPageName: () -> + if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + 'salesCurrentPage' + else if @activeTile() == @TILE_PAYMENTS_TO_YOU + 'distributionsCurrentPage' + else + 1 + + getCurrentList: () -> + if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + @state['sales'] + else if @activeTile() == @TILE_PAYMENTS_TO_YOU + @state['distributions'] + else + @state['sales'] + + currentNext: () -> + if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + @state.salesNext + else if @activeTile() == @TILE_PAYMENTS_TO_YOU + @state.distributionsNext + else + null + + onClick: (e) -> + e.preventDefault() + + context.location.href = '/client#/account' + getInitialState: () -> + { + user: null, + nextPager: null, + salesCurrentPage: 0, + distributionsCurrentPage: 0 + salesNext: null, + distributionsNext: null + sales: [], + distributions: [] + selected: 'payments to jamkazam', + updating: false, + billingInUS: true, + userWantsUpdateCC: false, + uncollectables: [] + } + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + + mainContent: () -> + if !@state.user? + `
Loading...
` + else if @state.selected == @TILE_PAYMENTS_TO_YOU + @paymentsToYou() + else if @state.selected == @TILE_PAYMENTS_TO_JAMKAZAM + @paymentsToJamKazam() + else if @state.selected == @TILE_PAYMENT_METHOD + @paymentMethod() + else + @paymentsToJamKazam() + + paymentsToYou: () -> + rows = [] + + for paymentHistory in @getCurrentList() + paymentMethod = 'Stripe' + + if paymentHistory.distributed + date = paymentHistory.teacher_payment.teacher_payment_charge.last_billing_attempt_at + status = 'Paid' + else + date = paymentHistory.created_at + if paymentHistory.not_collectable + status = 'Uncollectible' + else if !paymentHistory.teacher?.teacher?.stripe_account_id? + status = 'No Stripe Acct' + else + status = 'Collecting' + + + date = context.JK.formatDate(date, true) + description = paymentHistory.description + + if paymentHistory.teacher_payment? + amt = paymentHistory.teacher_payment.real_distribution_in_cents + else + amt = paymentHistory.real_distribution_in_cents + + displayAmount = ' $' + (amt/100).toFixed(2) + + amountClasses = {status: status} + + row = + ` + {date} + {paymentMethod} + {description} + {status} + {displayAmount} + ` + rows.push(row) + + `
+ + + + + + + + + + + + {rows} + + +
DATEMETHODDESCRIPTIONSTATUSAMOUNT
+ Next +
No more payment history
+
+ BACK +
+
+
` + + paymentMethod: () -> + disabled = @state.updating || @reuseStoredCard() + + submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating} + updateCardClassNames = {'button-grey': true, 'update-btn': true, disabled: disabled && @state.updating} + backClassNames = {'button-grey': true, disabled: disabled && @state.updating} + + cardNumberFieldClasses = {field: true, "card-number": true, error: @state.ccError} + expirationFieldClasses = {field: true, "expiration": true, error: @state.expiryError} + cvvFieldClasses = {field: true, "card-number": true, error: @state.cvvError} + inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError} + zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError} + nameClasses= {field: true, "name": true, error: @state.nameError} + formClasses= {stored: @reuseStoredCard()} + leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()} + rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()} + + if @state.uncollectables.length > 0 + uncollectable = @state.uncollectables[0] + uncollectableMessage = `
A charge for your music lesson with {uncollectable.teacher.name} failed. Please update your credit card information immediately so that we can pay the instructor. If you have called your credit card provider and believe there should be no problem with your card, please email us at support@jamkazam.com so that we can figure out what's gone wrong. Thank you!
` + + if @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0 + if @state.userWantsUpdateCC + header = 'Please update your billing address and payment information below.' + updateCardAction = `NEVERMIND` + actions = `
+ BACK + {updateCardAction} + SUBMIT CARD INFORMATION +
` + else + header = 'You have already entered a credit card in JamKazam.' + updateCardAction = `I'D LIKE TO UPDATE MY PAYMENT INFO` + actions = `
+ BACK + {updateCardAction} +
` + else + header = 'Please enter your billing address and payment information below.' + actions = `
+ BACKSUBMIT CARD INFORMATION +
` + if @state.shouldShowName && @state.user?.name? + username = @state.user?.name + nameField = + `
+ + +
` + + `
+
+ {uncollectableMessage} +
{header}
+
+ {nameField} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ {actions} +
+
+
` + + paymentsToJamKazam: () -> + rows = [] + uncollectables = [] + + for uncollectable in @state.uncollectables + date = context.JK.formatDate(uncollectable.last_billed_at_date, true) + paymentMethod = 'Credit Card' + amt = uncollectable.expected_price_in_cents + displayAmount = ' $' + (amt/100).toFixed(2) + + if uncollectable['is_card_declined?'] + reason = 'card declined' + else if uncollectable['is_card_expired?'] + reason = 'card expired' + else + reason = 'charge fail' + + row = + ` + {date} + {paymentMethod} + {uncollectable.description} + {reason} + {displayAmount} + ` + uncollectables.push(row) + + if uncollectables.length > 0 + uncollectableTable = ` +
+
You have unpaid lessons, which are listed immediately below. Click here to update your credit card info.
+
Unpaid Lessons
+ + + + + + + + + + + + {uncollectables} + +
CHARGED ATMETHODDESCRIPTIONREASONAMOUNT
+
Payments
+
` + for paymentHistory in @getCurrentList() + paymentMethod = 'Credit Card' + if paymentHistory.sale? + sale = paymentHistory.sale + amt = sale.recurly_total_in_cents + status = 'paid' + displayAmount = ' $' + (amt/100).toFixed(2) + date = context.JK.formatDate(sale.created_at, true) + items = [] + for line_item in sale.line_items + items.push(line_item.product_info?.name) + description = items.join(', ') + else + # this is a recurly webhook + transaction = paymentHistory.transaction + amt = transaction.amount_in_cents + status = transaction.transaction_type + displayAmount = '($' + (amt/100).toFixed(2) + ')' + date = context.JK.formatDate(transaction.transaction_at, true) + description = transaction.admin_description + + amountClasses = {status: status} + + row = + ` + {date} + {paymentMethod} + {description} + {status} + {displayAmount} + ` + rows.push(row) + + `
+ {uncollectableTable} + + + + + + + + + + + + {rows} + + +
DATEMETHODDESCRIPTIONSTATUSAMOUNT
+ Next +
No more payment history
+
+ BACK +
+
+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @getUncollectables() + @setState({selected: selection}) + + activeTile: (selected = this.state.selected) -> + if selected? + selected + else + @tiles()[-1] + + createTileLink: (i, tile) -> + if this.state.selected? + active = this.state.selected == tile + else + active = i == 0 + + + tileClasses = {activeTile: active, 'profile-tile': true} + tileClasses[@myRole()] = true + tileClasses = classNames(tileClasses) + + classes = classNames({last: i == @tiles().length - 1}) + + return `
{tile}
` + + tiles: () -> + if @viewerStudent() + tiles = @STUDENT_TILES + else + tiles = @TEACHER_TILES + tiles + + myRole: () -> + if @viewerStudent() + 'student' + else + 'teacher' + + viewerStudent: () -> + !@viewerTeacher() + + viewerTeacher: () -> + this.state.user?.is_a_teacher + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @tiles() + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNavClasses = {"profile-nav": true} + profileNavClasses[@myRole()] = true + profileNavClasses = classNames(profileNavClasses) + profileNav = `
+ {profileSelections} +
` + + `
+
+
payment
history:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` + + onUpdate: (e) -> + e.preventDefault() + + if this.state.updating + return + name = @root.find('input[name="name"]').val() + if @isSchoolManaged() + scheduling_communication = 'school' + else + scheduling_communication = 'teacher' + correspondence_email = @root.find('input[name="correspondence_email"]').val() + + @setState(updating: true) + rest.updateSchool({ + id: this.state.school.id, + name: name, + scheduling_communication: scheduling_communication, + correspondence_email: correspondence_email + }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + onUpdateDone: (response) -> + @setState({school: response, userSchedulingComm: null, schoolName: null, updateErrors: null, updating: false}) + + @app.layout.notify({title: "update success", text: "Your school information has been successfully updated"}) + + onUpdateFail: (jqXHR) -> + handled = false + + @setState({updating: false}) + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + handled = true + @setState({updateErrors: errors}) + + onSubmit: (e) -> + @resetErrors() + + e.preventDefault() + + if !window.Stripe? + @app.layout.notify({ + title: 'Payment System Not Loaded', + text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!" + }) + else + ccNumber = @root.find('input.card-number').val() + expiration = @root.find('input.expiration').val() + cvv = @root.find('input.cvv').val() + inUS = @root.find('input.billing-address-in-us').is(':checked') + zip = @root.find('input.zip').val() + + error = false + + if @state.shouldShowName + name = @root.find('#set-user-on-card').val() + + if name.indexOf('Anonymous') > -1 + @setState({nameError: true}) + error = true + + if !$.payment.validateCardNumber(ccNumber) + @setState({ccError: true}) + error = true + + bits = expiration.split('/') + + if bits.length == 2 + month = bits[0].trim(); + year = bits[1].trim() + + month = new Number(month) + year = new Number(year) + + if year < 2000 + year += 2000 + + if !$.payment.validateCardExpiry(month, year) + @setState({expiryError: true}) + error = true + else + @setState({expiryError: true}) + error = true + + + cardType = $.payment.cardType(ccNumber) + + if !$.payment.validateCardCVC(cvv, cardType) + @setState({cvvError: true}) + error = true + + if inUS && (!zip? || zip == '') + @setState({zipCodeError: true}) + + if error + return + + data = { + number: ccNumber, + cvc: cvv, + exp_month: month, + exp_year: year, + } + + @setState({updating: true}) + + window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response))); + + stripeResponseHandler: (status, response) -> + console.log("stripe response", JSON.stringify(response)) + + + if response.error + @setState({updating: false}) + if response.error.code == "invalid_number" + @setState({ccError: true, cvvError: null, expiryError: null}) + else if response.error.code == "invalid_cvc" + @setState({ccError: null, cvvError: true, expiryError: null}) + else if response.error.code == "invalid_expiry_year" || response.error.code == "invalid_expiry_month" + @setState({ccError: null, cvvError: null, expiryError: true}) + + #@setState({userWantsUpdateCC: false}) + #window.UserActions.refresh() + @storeCC(response.id) + + storeCC: (token) -> + if this.state.billingInUS + zip = @root.find('input.zip').val() + + data = { + token: token, + zip: zip, + test_drive: false, + normal: false + } + + if @state.shouldShowName + data.name = @root.find('#set-user-on-card').val() + + @setState({updating: true}) + rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR)) + + stripeSubmitted: (response) -> + @setState({updating: false}) + + logger.debug("stripe submitted: " + JSON.stringify(response)) + + @setState({userWantsUpdateCC: false}) + + #if @state.shouldShowName + window.UserActions.refresh() + + if response.uncollectables + context.JK.Banner.showAlert('Credit Card Updated', 'Than you. Your credit card info has been updated.

We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.') + else + @app.layout.notify({title: 'Credit Card Updated', text: 'Your credit card info has been updated.'}) + + + stripeSubmitFailure: (jqXHR) -> + @setState({updating: false}) + handled = false + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + if errors.errors.name? + @setState({name: errors.errors.name[0]}) + handled = true + else if errors.errors.user? + @app.layout.notify({title: "Can't Update Credit Card", text: "You " + errors.errors.user[0] + '.' }) + handled = true + + if !handled + @app.notifyServerError(jqXHR, 'Credit Card Not Stored') + + onUnlockPaymentInfo: (e) -> + e.preventDefault() + @setState({userWantsUpdateCC: true}) + + onLockPaymentInfo: (e) -> + e.preventDefault() + @setState({userWantsUpdateCC: false}) + + reuseStoredCard: () -> + !@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0 + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee similarity index 84% rename from web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee rename to web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee index 72edef0cd..e16c0126d 100644 --- a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee @@ -62,7 +62,7 @@ UserStore = context.UserStore @state.type == 'normal' isTestDrive: () -> - @state.type == 'test-drive' + @state.type?.indexOf('test-drive') == 0 parseId:(id) -> if !id? @@ -79,7 +79,6 @@ UserStore = context.UserStore @resetErrors() beforeShow: (e) -> - logger.debug("BookLesson: beforeShow", e.id) afterShow: (e) -> logger.debug("BookLesson: afterShow", e.id) @@ -181,6 +180,8 @@ UserStore = context.UserStore onBookLesson: (e) -> e.preventDefault() + logger.debug("user requested to book lesson") + if $(e.target).is('.disabled') return @@ -217,20 +218,26 @@ UserStore = context.UserStore else throw "Unable to determine lesson type" + logger.debug("lesson booking data: " + JSON.stringify(options)) @resetErrors() @setState({updating: true}) rest.bookLesson(options).done((response) => @booked(response)).fail((jqXHR) => @failedBooking(jqXHR)) booked: (response) -> @setState({updating: false}) + UserActions.refresh() if response.user['has_stored_credit_card?'] - context.location ="/client#/jamclass/lesson-session/" + response.id + context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email.") + url = "/client#/jamclass/lesson-booking/#{response.id}" + url = "/client#/jamclass" + context.location = url else - context.location = '/client#/jamclass/payment' + context.location = "/client#/jamclass/lesson-payment/lesson-booking_#{response.id}" failedBooking: (jqXHR) -> @setState({updating: false}) if jqXHR.status == 422 + logger.debug("unable to book lesson: " + jqXHR.responseText) body = JSON.parse(jqXHR.responseText) generalErrors = {errors: {}} @@ -260,9 +267,6 @@ UserStore = context.UserStore onCancel: (e) -> e.preventDefault() - isTestDrive: () -> - @state.type == 'test-drive' - isNormal: () -> @state.type == 'normal' @@ -306,11 +310,12 @@ UserStore = context.UserStore results render: () -> + teacher = @state.teacher + photo_url = teacher?.photo_url if !photo_url? photo_url = '/assets/shared/avatar_generic.png' - teacher = @state.teacher if teacher? name = `
{teacher.name}
` @@ -333,7 +338,7 @@ UserStore = context.UserStore am_pm = [``, ``] - bookLessonClasses = classNames({"button-orange": true, disabled: !this.state.teacher? && !@state.updating}) + bookLessonClasses = classNames({"button-orange": true, 'book-lesson-btn': true, disabled: !this.state.teacher? && !@state.updating}) cancelClasses = classNames({"button-grey": true, disabled: !this.state.teacher? && !@state.updating}) descriptionErrors = context.JK.reactSingleFieldErrors('description', @state.descriptionErrors) @@ -357,7 +362,7 @@ UserStore = context.UserStore
- +
@@ -371,7 +376,7 @@ UserStore = context.UserStore
- +
@@ -400,7 +405,7 @@ UserStore = context.UserStore
- +
@@ -414,7 +419,7 @@ UserStore = context.UserStore
- +
@@ -431,19 +436,52 @@ UserStore = context.UserStore if @state.user?.remaining_test_drives == 1 testDriveLessons = "1 TestDrive lesson credit" else - testDriveLessons = "#{this.state.user?.remaining_test_drives} TestDrive lesson credits" + testDriveLessons = "#{this.state.user.remaining_test_drives} TestDrive lesson credits" actions = `
CANCEL BOOK TESTDRIVE LESSON
` + testDriveCredits = 1 + + if this.state.user.lesson_package_type_id == 'test-drive' + testDriveCredits = 4 + else if this.state.user.lesson_package_type_id == 'test-drive-1' + testDriveCredits = 1 + else if this.state.user.lesson_package_type_id == 'test-drive-2' + testDriveCredits = 2 + + if this.state.user.remaining_test_drives > 0 + testDriveBookingInfo = `
+ +

You are booking a single 30-minute TestDrive session.

+ +

You currently have {testDriveLessons} available. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.
+ +

+

+
` + else + testDriveBookingInfo = `
+ +

You are booking a single 30-minute TestDrive session.

+ +

Once payment is entered on the next screen, the teacher will be notified, and this lesson will then use 1 of {testDriveCredits} TestDrive credits. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.
+ +

+

+
` + + columnLeft = `
{header} {slots}
Tell {teacher_first_name} a little about yourself as a student.
- + SEND + {closeBtn} + {emailSentNotice}
` + activeChannelType: () -> + if this.props?.channelType? + this.props.channelType + else + @state.channel + sendMessage:()-> if !context.JK.JamServer.connected return false @@ -92,7 +150,8 @@ ChatActions = @ChatActions if !@sendingMessage @sendingMessage = true - ChatActions.sendMsg(msg, @sendMsgDone, @sendMsgFail) + target_user_id = this.props?.other?.id + ChatActions.sendMsg(msg, @sendMsgDone, @sendMsgFail, target_user_id, @activeChannelType()) sendMsgDone: () -> @textBox.val('') @@ -109,6 +168,10 @@ ChatActions = @ChatActions e.preventDefault() @sendMessage() + handleCloseMessage: (e) -> + e.preventDefault() + this.props.onCloseClicked() + componentDidMount: () -> @root = $(@getDOMNode()) @textBox = @root.find('textarea') diff --git a/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee new file mode 100644 index 000000000..fc85530ab --- /dev/null +++ b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee @@ -0,0 +1,87 @@ +context = window + +@InLessonBroadcast = React.createClass({ + displayName: 'In Lesson Broadcast' + + getTimeRemaining: (t) -> + + if t < 0 + t = -t + + seconds = Math.floor( (t/1000) % 60 ); + minutes = Math.floor( (t/1000/60) % 60 ); + hours = Math.floor( (t/(1000*60*60)) % 24 ); + days = Math.floor( t/(1000*60*60*24) ); + + return { + 'total': t, + 'days': days, + 'hours': hours, + 'minutes': minutes, + 'seconds': seconds + }; + + + displayTime: () -> + if @props.lessonSession.initialWindow + # offset time by 10 minutes to get the 'you need to wait message' in + untilTime = @getTimeRemaining(@props.lessonSession.until.total + (10 * 60 * 1000)) + else + untilTime = @props.lessonSession.until + timeString = '' + if untilTime.days != 0 + timeString += "#{untilTime.days} days, " + if untilTime.hours != 0 || timeString.length > 0 + timeString += "#{untilTime.hours} hours, " + if untilTime.minutes != 0 || timeString.length > 0 + timeString += "#{untilTime.minutes} minutes, " + if untilTime.seconds != 0 || timeString.length > 0 + timeString += "#{untilTime.seconds} seconds" + + if timeString == '' + 'now!' + timeString + + render: () -> + console.log("@props.lessonSession", @props.lessonSession) + if @props.lessonSession.completed + if @props.lessonSession.success + content = `
+

This lesson is over.

+
` + else + content = `
+

This lesson is over, but will not be billed.

+
` + else if @props.lessonSession.beforeSession + content = `
+

This lesson will start in:

+

{this.displayTime()}

+
` + else if @props.lessonSession.initialWindow + content = `
+

You need to wait in this session for

+

{this.displayTime()}

+

to allow time for your teacher to join you. If you leave before this timer reaches zero, and your teacher joins this session, you will be marked absent and charged for the lesson.

+
` + else if @props.lessonSession.teacherFault + if @props.lessonSession.teacherPresent? + content = `
+

Because your teacher was late, you may now leave the session. However, if you choose to stay in the session with the teacher, after 5 minutes together the session will be considered a success, and you will be billed.

+

If the two of you do not spend at least 5 minutes together in the session, your teacher will be marked absent and penalized for missing the lesson, and you will not be charged for this lesson.

+
` + else + content = `
+

You may now leave the session.

+

Your teacher will be marked absent and penalized for missing the lesson. You will not be charged for this lesson.

+

We apologize for your inconvenience, and we will work to remedy this situation.

+
` + + if content? + `
+ {content} +
` + else + null + +}) diff --git a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee new file mode 100644 index 000000000..e96a1c6d1 --- /dev/null +++ b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee @@ -0,0 +1,474 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@JamClassScreen = React.createClass({ + + mixins: [ + @ICheckMixin, + @PostProcessorMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + lookup : { + name_specified: {name: 'Profile', profile: ''}, + experiences_teaching: {name: 'Experience', teacher_profile: "experience"}, + experiences_education: {name: 'Experience', teacher_profile: "experience" }, + experiences_award: {name: 'Experience', teacher_profile: "experience" }, + has_stripe_account: {name: 'Stripe', text: "Press the 'Stripe Connect' button in the bottom-left of the page"}, + has_teacher_bio: {name: 'Introduction', teacher_profile: "introduction"}, + intro_video: {name: 'Introduction', teacher_profile: "introduction"}, + years_teaching: {name: 'Introduction', teacher_profile: "introduction"}, + years_playing: {name: 'Introduction', teacher_profile: "introduction"}, + instruments_or_subject: {name: 'Basics', teacher_profile: "basics"}, + genres: {name: 'Basics', teacher_profile: "basics"}, + languages: {name: 'Basics', teacher_profile: "basics"}, + teaches_ages_specified: {name: 'Basics', teacher_profile: "basics"}, + teaching_level_specified: {name: 'Basics', teacher_profile: "basics"}, + has_pricing_specified: {name: 'Pricing', teacher_profile: "pricing"} + } + + onAppInit: (@app) -> + @app.bindScreen('jamclass', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + @setState({user: userState?.user}) + + componentDidMount: () -> + @root = $(@getDOMNode()) + + componentDidUpdate: () -> + items = @root.find('.jamtable tbody td.actionsColumn .lesson-session-actions-btn') + + $.each(items, (i, node) => ( + $node = $(node) + + lesson = @findLesson($node.attr('data-lesson-id')) + + $node.lessonSessionActions(lesson).on(context.JK.EVENTS.LESSON_SESSION_ACTION, @lessonSessionActionSelected) + )) + + lessonSessionActionSelected: (e, data) -> + + lessonId = data.options.id + lesson = @findLesson(lessonId) + + if data.lessonAction == 'status' + # show the status of the lesson + window.location.href = '/client#/jamclass/lesson-booking/' + lessonId + else if data.lessonAction == 'messages' + @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lessonId}) + else if data.lessonAction == 'cancel' + @cancelLesson(lesson) + # @@app.layout.showDialog('cancel-lesson-dialog', {d1: lessonId}) + else if data.lessonAction == 'join' + window.location.href = '/client#/session/' + lesson.music_session.id + else if data.lessonAction == 'reschedule' + @rescheduleLesson(lesson) + else if data.lessonAction == 'start-5-min' + rest.lessonStartTime({id: lessonId, minutes: 5}).done((response) => (@app.layout.notify({title: 'Start Time Set', text: "Start time for session set to 5 mins from now"}))) + else if data.lessonAction == 'start-35-ago' + rest.lessonStartTime({id: lessonId, minutes: -35}).done((response) => (@app.layout.notify({title: 'Start Time Set', text: "Start time for session set to 35 mins ago"}))) + else if data.lessonAction == 'enter-payment' + window.location.href = "/client#/jamclass/lesson-payment/lesson-booking_#{lessonId}" + else + context.JK.showAlert('unknown lesson action', 'The option in the menu is unknown') + + findLesson: (lessonId) -> + for lesson in @lessons() + if lessonId == lesson.id + return lesson + + return null + + rescheduleSelected: (lesson, recurring) -> + rest.checkLessonReschedule({id: lesson.id, update_all: recurring}) + .done((response) => ( + if recurring + window.location.href = '/client#/jamclass/lesson-booking/' + lesson.lesson_booking_id + else + window.location.href = '/client#/jamclass/lesson-booking/' + lesson.id + )) + .fail((jqXHR) => ( + if jqXHR.status == 422 + + if recurring + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.

") + else + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.

") + else + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.

") + else + @app.ajaxError(jqXHR) + )) + + refreshLesson: (lessonId) -> + rest.getLesson({id: lessonId}).done((response) => @refreshLessonDone(response)).fail((jqXHR) => @refreshLessonFail(jqXHR)) + + refreshLessonDone: (lesson_session) -> + @updateLessonState(lesson_session) + + refreshLessonFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + issueCancelLesson: (lesson, update_all) -> + request = {} + request.message = '' + request.id = lesson.lesson_booking_id + request.lesson_session_id = lesson.id + request.update_all = update_all + + rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response, lesson)).fail((response) => @cancelLessonBookingFail(response)) + + cancelLessonBookingDone: (booking, lesson) -> + if booking.focused_lesson.teacher_short_canceled + if @teacherViewing() + context.JK.Banner.showAlert('late cancellation warning', 'Cancelling a lesson less than 24 hours before it’s scheduled to start should be avoided, as it’s an inconvenience to the student. Repeated violations of this policy will negatively affect your teacher score.') + + @refreshLesson(lesson.id) + + cancelLessonBookingFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + cancelSelected: (lesson, recurring) -> + rest.checkLessonCancel({id: lesson.id, update_all: recurring}).done((response) => (@issueCancelLesson(lesson, recurring))).fail((jqXHR) => (@cancelSelectedFail(jqXHR))) + + cancelSelectedFailed: (jqXHR) -> + if jqXHR.status == 422 + + if recurring + if @viewerStudent() + buttons = [] + buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) + buttons.push({name: 'CANCEL RECURRING LESSONS', buttonStyle: 'button-orange', click:(() => (@issueCancelLesson(lesson, true)))}) + context.JK.Banner.show({title: "Policy Issue", html: "You may cancel this recurring series of lessons, but it is too late to cancel the next scheduled lesson because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.", buttons: buttons}) + else + # path should not be taken + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + # path should not be taken + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.

") + else + @app.ajaxError(jqXHR) + + rescheduleLesson: (lesson) -> + if lesson.recurring + buttons = [] + buttons.push({name: 'THIS SESSION', buttonStyle: 'button-orange', click:(() => (@rescheduleSelected(lesson, false)))}) + buttons.push({name: 'ALL SESSIONS', buttonStyle: 'button-orange', click:(() => (@rescheduleSelected(lesson, true)))}) + context.JK.Banner.show({title: 'Rescheduling Selection', html:'Do you wish to all reschedule all lessons or just the one selected?', buttons: buttons}) + else + @rescheduleSelected(lesson, false) + + + cancelLesson: (lesson) -> + + if lesson.isRequested + confirmTitle = 'Confirm Decline' + verbLower = 'decline' + else + confirmTitle = 'Confirm Cancelation' + verbLower = 'cancel' + if !lesson.isRequested || lesson.recurring + buttons = [] + buttons.push({name: 'CANCEL', buttonStyle: 'button-grey'}) + buttons.push({name: 'THIS SESSION', buttonStyle: 'button-orange', click:(() => (@cancelSelected(lesson, false)))}) + buttons.push({name: 'ALL SESSIONS', buttonStyle: 'button-orange', click:(() => (@cancelSelected(lesson, true)))}) + context.JK.Banner.show({title: 'Select One', html:"Do you wish to all #{verbLower} all lessons or just the one selected?", buttons: buttons}) + else + context.JK.Banner.showYesNo({title: confirmTitle, html:"Are you sure you want to #{verbLower} this lesson?", yes: () =>(@cancelSelected(lesson, lesson.recurring))}) + + getInitialState: () -> + { + user: null, + } + + beforeHide: (e) -> + + + beforeShow: (e) -> + + afterShow: (e) -> + @checkStripeSuccessReturn() + @setState({updating: true}) + rest.getLessonSessions().done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR)) + + checkStripeSuccessReturn: () -> + if $.QueryString['stripe-success']? + if $.QueryString['stripe-success'] == 'true' + context.JK.Banner.showNotice('stripe connected', 'Congratulations, you have successfully connected your Stripe account, so payments for student lessons can now be processed.') + + if window.history.replaceState #ie9 proofing + window.history.replaceState({}, "", "/client#/jamclass") + + resetState: () -> + @setState({updating: false, lesson: null}) + + jamClassLoaded: (response) -> + @setState({updating: false}) + + @postProcess(response) + + @setState({lesson_sessions: response}) + + failedJamClassLoad: (jqXHR) -> + @setState({updating: false}) + if jqXHR.status == 404 + @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"}) + + lessons: () -> + if @state.lesson_sessions? + @state.lesson_sessions.entries + else + [] + + postProcess: (data) -> + + for lesson in data.entries + + # calculate: + # .other (user object), + # .me (user object), + # other/me.musician_profile (link to musician profile) + # other/me.resolved_photo_url + # .hasUnreadMessages + # .isRequested (doesn't matter if countered or not). but can't be done + # .isScheduled (doesn't matter if countered or not). + @postProcessLesson(lesson) + + onReadMessage: (lesson_session, e) -> + e.preventDefault() + + data = {id: lesson_session.id} + data["#{this.myRole()}_unread_messages"] = false + + rest.updateLessonSessionUnreadMessages(data).done((response) => @updatedLessonSessionReadDone(response)).fail((jqXHR) => @updatedLessonSessionReadFail(jqXHR)) + + updateLessonState: (lesson_session) -> + @postProcessLesson(lesson_session) + + for lesson in @lessons() + if lesson.id == lesson_session.id + $.extend(lesson, lesson_session) + @setState({lesson_sessions: this.state.lesson_sessions}) + + updatedLessonSessionReadDone: (lesson_session) -> + # update lesson session data in local state + + @updateLessonState(lesson_session) + + @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lesson_session.id}) + + + updatedLessonSessionReadFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + + openMenu: (lesson, e) -> + $this = $(e.target) + if !$this.is('.lesson-session-actions-btn') + $this = $this.closest('.lesson-session-actions-btn') + $this.btOn() + + constructMissingLinks: (user) -> + + links = [] + + matches = {} + if user.teacher? + for problem, isPresent of user.teacher.profile_pct_summary + data = @lookup[problem] + if data? && !isPresent + matches[data.name] = data + + for name, data of matches + if !data.text? + links.push(`{data.name}`) + + for name, data of matches + if data.text? + links.push(`{data.name}`) + `
    {links}
` + else + null + + onProfileHelp: (data, e) -> + e.preventDefault() + @app.layout.notify({title: data.name, text: data.text}) + + missingProfileClick: (data, e) -> + if data.profile? + window.ProfileActions.startProfileEdit(data.profile, true) + else if data.teacher_profile? + window.ProfileActions.startTeacherEdit(data.teacher_profile, true) + else + throw "unknown type in data " + JSON.stringify(data) + + viewTeacherProfile: (e) -> + e.preventDefault() + window.ProfileActions.viewTeacherProfile(this.state.user, '/client#/jamclass', 'BACK TO JAMCLASS HOME') + + render: () -> + + disabled = @state.updating + + if !@state.user?.id + return `
Loading
` + + if @viewerStudent() + searchTeachers = `
+

search teachers

+ +

JamClass instructors are each individually screened to ensure that they are highly qualified music + teachers, + equipped to teach effectively online, and background checked. +

+ + +
` + + if @state.user?['can_buy_test_drive?'] + rightContent = `
+ +

+ JamClass is the best way to teach or take online music lessons. Learn more about teaching or taking lessons. +

+ +

TestDrive offers 3 unbeatable options for students to try JamClass online lessons, and to find the best teacher:

+
    +
  1. Take 4 full 30-minute lessons from 4 different teachers. Just $49.99. Great for making sure you click with the best teacher for you.
  2. +
  3. Take 2 full 30-minute lessons from 2 different teachers. Just $29.99. Two lessons for the price of one or try two different teachers.
  4. +
  5. Take 1 full 30-minute lesson for $9.99. Great value for the online experience if you're sure which teacher you want.
  6. +
+ + +
` + else + rightContent = `
+

Read our JamClass user guide for students to learn how to get the most out of your online lessons! +

+

If you have any problems, please email us at support@jamkazam.com. We're here to help make your lessons a great experience.

+
` + + else + searchTeachers = `
+

stripe status

+
+ +
+
` + if this.state.user? + teacherProfileUri = "/client#/profile/teacher/#{this.state.user.id}" + pct = Math.round(this.state.user.teacher?.profile_pct) + + if !pct? + pct = 0 + + if pct == 100 + pctCompleteMsg = `

Your teacher profile is {pct}% complete.

` + else + pctCompleteMsg = `

Your teacher profile is {pct}% complete. The following sections of your profile are missing information. Click any of these links to view and add missing information:

` + missingLinks = @constructMissingLinks(this.state.user) + + rightContent = `
+ {pctCompleteMsg} + {missingLinks} + VIEW TEACHER PROFILE +

+ Read our JamClass user guide for teachers to learn how to get the most out of the JamClass technology and marketplace. +

+

+ If you have any problems, please email us support@jamkazam.com. We're here to help you build your lesson business. +

+
` + + classes = [] + if @state.updating + classes = [` + Loading... + `] + else + for lessonData in @lessons() + + if lessonData.hasUnreadMessages + unreadMessages = `` + else + unreadMessages = null + lesson = ` +
{lessonData.other.first_name}
{lessonData.other.last_name}
+ {lessonData.music_session.pretty_scheduled_start_with_timezone} + {lessonData.displayStatus} + {unreadMessages} + menu
+ ` + + classes.push(lesson) + if classes.length == 0 + classes = [` + No Lessons Yet + `] + + `
+
+

my lessons

+
+ + + + + + + + + + + + {classes} + +
{this.otherRole()}DATE/TIMESTATUSACTIONS
+ + +
+ {searchTeachers} +
+
+ {rightContent} +
+
+
` + + + viewerStudent: () -> + !@viewerTeacher() + + viewerTeacher: () -> + this.state.user?.is_a_teacher + + myRole: () -> + if @viewerStudent() + 'student' + else + 'teacher' + otherRole: () -> + if @viewerStudent() + 'teacher' + else + 'student' +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee deleted file mode 100644 index 8fef2d1fa..000000000 --- a/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee +++ /dev/null @@ -1,154 +0,0 @@ -context = window -rest = context.JK.Rest() -logger = context.JK.logger - -UserStore = context.UserStore - -@JamClassStudentScreen = React.createClass({ - - mixins: [ - @ICheckMixin, - Reflux.listenTo(AppStore, "onAppInit"), - Reflux.listenTo(UserStore, "onUserChanged") - ] - - onAppInit: (@app) -> - @app.bindScreen('jamclass', - {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) - - onUserChanged: (userState) -> - @setState({user: userState?.user}) - - componentDidMount: () -> - - componentDidUpdate: () -> - - getInitialState: () -> - { - user: null, - } - - beforeHide: (e) -> - - - beforeShow: (e) -> - - afterShow: (e) -> - @setState({updating: true}) - rest.getLessonSessions().done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR)) - - resetState: () -> - @setState({updating: false, lesson: null}) - - jamClassLoaded: (response) -> - @setState({updating: false}) - @setState({summary: response}) - - failedJamClassLoad: (jqXHR) -> - @setState({updating: false}) - @setState({summary: response}) - if jqXHR.status == 404 - @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"}) - - render: () -> - disabled = @state.updating - - classes = [] - if @state.updating - classes = [`Loading...`] - else - - `
-
-
-

my lessons

- - - - - - - - - - - - {classes} - -
TEACHERDATE/TIMESTATUSACTIONS
- - -
-
-

search teachers

- -

JamClass instructors are each individually screened to ensure that they are highly qualified music - teachers, - equipped to teach effectively online, and background checked. -

- - -
-
-
-
-

learn about jamclass

- -

- JamClass is the best way to make music lessons, offering significant advantadges over both traditional - face-to-face lessons - and online skype lessons. -

- - -
-
-

sign up for testdrive

- -

- There are two awesome, painless ways to get started with JamClass. -

- -

- Sign up for TestDrive and take 4 full 30-minute lessons - one each from 4 different instructors - for just - $49.99. - You wouldn't marry the first person you date, right? Find the best teacher for you. It's the most important - factor in the success for your lessons! -

- -

- Or take one JamClass lesson free. It's on us! We're confident you'll take more. -

- -

- Sign up for TestDrive using the button below, or to take one free lesson, search our teachers, and click the - Book Free Lesson on your favorite. -

- - -
-
-

get ready for your first lesson

- -

Be sure to set up and test the JamKazam app in an online music session a few days before - your first lesson! We're happy to help, and we'll even get in a session with you to make sure everything - is working properly. Ping us at support@jamkazam.com anytime, and - read our - JamClass user guide to learn how to use all the lesson features. -

-
-
-
-
` - -}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee index 3500dfffc..bca5cb162 100644 --- a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee @@ -21,6 +21,9 @@ UserStore = context.UserStore onSlotDecision: (slot_decision) -> @setState({slot_decision: slot_decision?.slot_decision}) + onUpdateAllDecision: (update_all) -> + @setState({update_all: update_all?.update_all}) + componentWillMount: () -> componentDidMount: () -> @@ -75,7 +78,15 @@ UserStore = context.UserStore id: e.id, }).done((response) => @getLessonBookingDone(response)).fail(@app.ajaxError) + hasFocusedLesson: () -> + this.state.booking.focused_lesson?.id? + + focusedLesson: () -> + this.state?.booking?.focused_lesson + updateBookingState: (booking) -> + console.log("updating booking state", booking) + if booking.counter_slot? startSlotDecision = booking.counter_slot.id else @@ -84,7 +95,9 @@ UserStore = context.UserStore else startSlotDecision = booking.default_slot.id - @setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false}) + update_all = !booking.focused_lesson?.id? + + @setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false, update_all: update_all}) getLessonBookingDone: (response) -> @updateBookingState(response) @@ -105,13 +118,13 @@ UserStore = context.UserStore request.id = this.state.booking.id request.timezone = window.jstz.determine().name() request.message = @getMessage() - request.update_all = true + request.lesson_session_id = @focusedLesson()?.id rest.counterLessonBooking(request).done((response) => @counterLessonBookingDone(response)).fail((response) => @counterLessonBookingFail(response)) else if @state.slot_decision == 'decline' request = {} request.message = @getMessage() request.id = this.state.booking.id - request.update_all = true + request.lesson_session_id = @focusedLesson()?.id rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response)).fail((response) => @cancelLessonBookingFail(response)) else if @state.slot_decision request = {} @@ -187,6 +200,7 @@ UserStore = context.UserStore minute = $slot.find('.minute').val() am_pm = $slot.find('.am_pm').val() + update_all = $slot.find('input.update-all').is(':checked') && @isRecurring() if hour? and hour != '' hour = new Number(hour) @@ -208,7 +222,7 @@ UserStore = context.UserStore day_of_week = $slot.find('.day_of_week').val() - {hour: hour, minute: minute, date: date, day_of_week: day_of_week} + {hour: hour, minute: minute, date: date, day_of_week: day_of_week, update_all: update_all} student: () -> @state.booking?.user @@ -323,9 +337,16 @@ UserStore = context.UserStore lessonType = "single #{this.lessonLength()}-minute lesson" bookedPrice: () -> - this.state.booking?.booked_price + price = this.state.booking?.booked_price + if price? + if typeof price == "string" + price = new Number(price) + return price.toFixed(2) + else + return price lessonPaymentAmt: () -> + console.log("lessonPaymentAmt") if @state.booking?.payment_style == 'elsewhere' '$10' else if @state.booking?.payment_style == 'single' @@ -333,7 +354,7 @@ UserStore = context.UserStore else if @state.booking?.payment_style == 'weekly' "at $#{this.bookedPrice()} per lesson" else if @state.booking?.payment_style == 'monthly' - "monthly at $#{this.bookedPrice()} per month" + "monthly at $#{this.bookedPrice()} per month" else "$???" @@ -400,11 +421,18 @@ UserStore = context.UserStore "#{context.JK.padString(hour.toString(), 2)}:#{context.JK.padString(slot.minute.toString(), 2)}#{am_pm} (#{slot.timezone})" + displayableLesson: () -> + lesson = @focusedLesson() + + if !lesson? + lesson = @state.booking.next_lesson + + lesson isStartingSoonOrNow: () -> # 60 minutes before - startTime = new Date(@state.booking.next_lesson.scheduled_start).getTime() + startTime = new Date(@displayableLesson().scheduled_start).getTime() endTime = (startTime + @lessonLength() * 60) now = new Date().getTime() @@ -412,17 +440,17 @@ UserStore = context.UserStore isNow: () -> - startTime = new Date(@state.booking.next_lesson.scheduled_start).getTime() + startTime = new Date(@displayableLesson().scheduled_start).getTime() endTime = (startTime + @lessonLength() * 60) now = new Date().getTime() now > startTime && now < endTime isPast: () -> - new Date().getTime() > new Date(@state.booking.next_lesson.scheduled_start).getTime() + new Date().getTime() > new Date(@displayableLesson().scheduled_start).getTime() sessionLink: () -> - link = "/client#/session/#{this.stateb.booking.next_lesson.music_session_id}" + link = "/client#/session/#{this.displayableLesson().music_session_id}" `JOIN SESSION` @@ -439,9 +467,9 @@ UserStore = context.UserStore else if @isPast() data =`

This lesson is over.

` else - data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + data = `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` else if @isRequested() - data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + data = `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` `
{data}
` @@ -452,9 +480,9 @@ UserStore = context.UserStore else if @isPast() `

This lesson is over.

` else - `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` else if @isRequested() - `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` renderCancelLesson: () -> `
@@ -490,10 +518,12 @@ UserStore = context.UserStore decisionProps: (slots) -> { onSlotDecision: this.onSlotDecision, + onUpdateAllDecision: this.onUpdateAllDecision, initial: this.neverAccepted(), counter: this.isCounter(), is_recurring: this.isRecurring(), slot_decision: this.state.slot_decision, + update_all: this.state.update_all, slots: slots, otherRole: this.otherRole(), onUserDecision: this.onAccept, @@ -501,18 +531,26 @@ UserStore = context.UserStore disabled: this.state.updatingLesson, selfLastToAct: this.selfLastToAct(), counterErrors: this.state.counterErrors, - cancelErrors: this.state.cancelErrors + cancelErrors: this.state.cancelErrors, + focusedLesson: this.focusedLesson() } render: () -> if @state.updating - @renderLoading() + + return @renderLoading() + else if @teacherViewing() - @renderTeacher() + + return @renderTeacher() + else if @studentViewing() - @renderStudent() + + return @renderStudent() + else - @renderLoading() + + return @renderLoading() renderTeacher: () -> @@ -546,7 +584,7 @@ UserStore = context.UserStore header = 'This lesson has been suspended' content = @renderTeacherSuspended() - `
+ return `
` renderLoading: () -> - header = 'Loading...' + header = 'Loading ...' `
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 b166a9404..384ce3b9e 100644 --- a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -51,7 +51,9 @@ UserStore = context.UserStore lesson: null, updating: false, billingInUS: true, - userWantsUpdateCC: false + userWantsUpdateCC: false, + "test-drive": false, + teacher: null } beforeHide: (e) -> @@ -62,8 +64,51 @@ UserStore = context.UserStore afterShow: (e) -> @resetState() @resetErrors() - @setState({updating: true}) - rest.getUnprocessedLessonOrIntent().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR)) + parsed = @parseId(e.id) + parsed.updating = false + + if parsed['lesson-booking'] + parsed.updating = true + rest.getLessonBooking({id: parsed.lesson_booking_id}).done((response) => @lessonBookingLoaded(response)).fail((jqXHR) => @failedLessonBooking(jqXHR)) + else if parsed['teacher-intent'] + parsed.updating = true + rest.getUserDetail({id: parsed.teacher_id}).done((response) => @teacherLoaded(response)).fail((jqXHR) => @failedTeacher(jqXHR)) + else if parsed['test-drive'] + logger.debug("test-drive lesson payment; no teacher/booking in context") + else + logger.error("unknown state for lesson-payment") + window.location.href = '/client#/jamclass' + + @setState(parsed) + + parseId: (id) -> + result = {} + + # id can be: + # 'test-drive' + # or 'lesson-booking_id' + # or 'teacher_id + + result['test-drive'] = false + result['lesson-booking'] = false + result['teacher-intent'] = false + + bits = id.split('_') + if bits.length == 1 + # should be id=test-drive + result[id] = true + else if bits.length == 2 + type = bits[0] + if type == 'lesson-booking' + result[type] = true + result.lesson_booking_id = bits[1] + else if type == 'teacher' + result['teacher-intent'] = true + result.teacher_id = bits[1] + + logger.debug("LessonPayment: parseId " + JSON.stringify(result)) + + result resetErrors: () -> @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null}) @@ -74,25 +119,34 @@ UserStore = context.UserStore @setState({billingInUS: checked}) resetState: () -> - @setState({updating: false, lesson: null}) + @setState({updating: false, lesson: null, teacher: null, "test-drive": false, "lesson-booking" : false, "teacher-intent": false}) - unprocessLoaded: (response) -> + lessonBookingLoaded: (response) -> @setState({updating: false}) - logger.debug("unprocessed loaded", response) - @setState(response) + logger.debug("lesson booking loaded", response) - failedBooking: (jqXHR) -> - @setState({updating: false}) - @setState({lesson: null}) - if jqXHR.status == 404 - # no unprocessed lessons. That's arguably OK; the user is just going to enter their info up front. - console.log("nothing") + if response.card_presumed_ok + context.JK.Banner.showNotice("Lesson Already Requested", "You have already requested this lesson from this teacher.") + window.location.href = "/client#/jamclass" + @setState({lesson: response, teacher: response.teacher}) - failedUnprocessLoad: (jqXHR) -> + failedLessonBooking: (jqXHR) -> @setState({updating: false}) @app.layout.notify({ - title: 'Unable to load lesson', - text: 'Please attempt to book a free lesson first or refresh this page.' + title: 'unable to load lesson info', + text: 'Something has gone wrong. Please try refreshing the page.' + }) + + teacherLoaded: (response) -> + @setState({updating: false}) + logger.debug("teacher loaded", response) + @setState({teacher: response}) + + failedTeacher: (jqXHR) -> + @setState({updating: false}) + @app.layout.notify({ + title: 'unable to load teacher info', + text: 'Something has gone wrong. Please try refreshing the page.' }) onBack: (e) -> @@ -110,7 +164,8 @@ UserStore = context.UserStore text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!" }) else - if !@state.userWantsUpdateCC + + if @reuseStoredCard() @attemptPurchase(null) else @@ -173,12 +228,16 @@ UserStore = context.UserStore exp_year: year, } + @setState({updating: true}) + window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response))); stripeResponseHandler: (status, response) -> - console.log("response", response) + console.log("stripe response", JSON.stringify(response)) if response.error + @setState({updating: false}) + if response.error.code == "invalid_number" @setState({ccError: true, cvvError: null, expiryError: null}) else if response.error.code == "invalid_cvc" @@ -188,6 +247,12 @@ UserStore = context.UserStore else @attemptPurchase(response.id) + isNormal: () -> + @state.lesson?.lesson_type == 'paid' + + isTestDrive: () -> + @state['test-drive'] == true || @state.lesson?.lesson_type == 'test-drive' || @state['teacher-intent'] + attemptPurchase: (token) -> if this.state.billingInUS zip = @root.find('input.zip').val() @@ -195,50 +260,83 @@ UserStore = context.UserStore data = { token: token, zip: zip, - test_drive: @state.lesson?.lesson_type == 'test-drive' || (@state.intent?.intent == 'book-test-drive') + test_drive: @isTestDrive(), + booking_id: @state.lesson?.id, + normal: @isNormal() } if @state.shouldShowName data.name = @root.find('#set-user-on-card').val() + @setState({updating: true}) rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR)) stripeSubmitted: (response) -> - logger.debug("stripe submitted", response) + @setState({updating: false}) - if @state.shouldShowName - window.UserActions.refresh() + logger.debug("stripe submitted: " + JSON.stringify(response)) + + #if @state.shouldShowName + window.UserActions.refresh() # if the response has a lesson, take them there - if response.lesson?.id? - context.Banner.showNotice({ - title: "Lesson Requested", - text: "The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email." - }) - window.location = "/client#/jamclass/lesson-session/" + response.lesson.id - else if response.test_drive? || response.intent_book_test_drive? + if response.test_drive? + # ok, they bought a package + if response.lesson_package_type? + # always of form test-drive-# + prefixLength = "test-drive-".length + packageLength = response.lesson_package_type.package_type.length - if response.test_drive?.teacher_id - teacher_id = response.test_drive?.teacher_id - else if response.intent_book_test_drive? - teacher_id = response.intent_book_test_drive?.teacher_id + logger.debug("prefix: " + prefixLength.toString()) + logger.debug("package: " + packageLength.toString()) + testDriveCount = response.lesson_package_type.package_type.substring(prefixLength, packageLength) - if teacher_id? - text = "You now have 4 lessons that you can take with 4 different teachers.

We've taken you automatically to the lesson booking screen for the teacher you initially showed interest in." - location = "/client#/profile/teacher/" + teacher_id - location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id + logger.debug("testDriveCount: " + testDriveCount) + + if response.test_drive?.teacher_id + teacher_id = response.test_drive.teacher_id + if testDriveCount == 1 + text = "You have purchased a TestDrive credit and have used it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly." + else + text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly." + location = "/client#/jamclass" + else + if @state.teacher?.id + + # the user bought the testdrive, and there is a teacher of interest in context (but no booking) + if testDriveCount == 1 + text = "You now have 1 TestDrive credit.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." + location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id + else + text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." + location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id + else + # the user bought test drive, but 'cold' , i.e., no teacher in context + if testDriveCount == 1 + text = "You now have 1 TestDrive credit.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." + location = "/client#/teachers/search" + else + text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." + location = "/client#/teachers/search" + + context.JK.Banner.showNotice("Test Drive Purchased",text) + window.location = location else - text = "You now have 4 lessons that you can take with 4 different teachers.

We've taken you automatically to the Teacher Search screen, so you can search for teachers right for you." - location = "/client#/teachers/search" - context.JK.Banner.showNotice({ - title: "Test Drive Purchased", - text: text - }) - window.location = location + context.JK.Banner.showNotice("Something Went Wrong", "Please email support@jamkazam.com and indicate that your attempt to buy a TestDrive failed") + window.location = "/client#/jamclass/" + + else if response.lesson?.id? + context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email.") + + url = "/client#/jamclass/lesson-booking/" + response.lesson.id + url = "/client#/jamclass" + window.location.href = url + else window.location = "/client#/teachers/search" stripeSubmitFailure: (jqXHR) -> + @setState({updating: false}) handled = false if jqXHR.status == 422 errors = JSON.parse(jqXHR.responseText) @@ -263,6 +361,16 @@ UserStore = context.UserStore reuseStoredCard: () -> !@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] + bookedPrice: () -> + booked_price = this.state.lesson.booked_price + + if booked_price? + if typeof booked_price == "string" + booked_price = new Number(booked_price) + return booked_price.toFixed(2) + else + return '??' + render: () -> disabled = @state.updating || @reuseStoredCard() @@ -276,45 +384,31 @@ UserStore = context.UserStore {name}
` else - if @state.lesson? || @state.intent? - if @state.lesson? - photo_url = @state.lesson.teacher.photo_url - name = @state.lesson.teacher.name - lesson_length = this.state.lesson.lesson_length - lesson_type = this.state.lesson.lesson_type - else - photo_url = @state.intent.teacher.photo_url - name = @state.intent.teacher.name - lesson_length = 30 - lesson_type = @state.intent.intent.substring('book-'.length) - - if !photo_url? - photo_url = '/assets/shared/avatar_generic.png' - teacherDetails = `
-
- -
- {name} -
` + if @state.lesson? || @state['test-drive'] || @state.teacher? + if @state.teacher? + photo_url = @state.teacher.photo_url + name = @state.teacher.name - if lesson_type == 'single-free' - header = `

enter card info

+ if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' -
Your card wil not be charged.
See explanation to the right.
+ teacherDetails = `
+
+ +
+ {name}
` - bookingInfo = `

You are booking a single free {lesson_length}-minute lesson.

` - bookingDetail = `

To book this lesson, you will need to enter your credit card information. - You will absolutely not be charged for this free lesson, and you have no further commitment to purchase - anything. We have to collect a credit card to prevent abuse by some users who would otherwise set up - multiple free accounts to get multiple free lessons. -
-

-

` - else if lesson_type == 'test-drive' + if @state.lesson? + lesson_length = @state.lesson.lesson_length + lesson_type = @state.lesson.lesson_type + else + lesson_length = 30 + lesson_type = 'test-drive' + if @isTestDrive() + if @reuseStoredCard() header = `

purchase test drive

` else @@ -329,12 +423,16 @@ UserStore = context.UserStore jamclass policies

` - else if lesson_type == 'paid' - header = `

enter payment info for lesson

` + else if @isNormal() + if @reuseStoredCard() + header = `

purchase lesson

` + else + header = `

enter payment info for lesson

` + if this.state.lesson.recurring if this.state.lesson.payment_style == 'single' bookingInfo = `

You are booking a {lesson_length} minute lesson for - ${this.state.lesson.booked_price.toFixed(2)}

` + ${this.bookedPrice()}

` bookingDetail = `

Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your lesson is scheduled, or you will be charged for the lesson in full. @@ -368,7 +466,7 @@ UserStore = context.UserStore

` else bookingInfo = `

You are booking a {lesson_length} minute lesson for - ${this.state.lesson.booked_price.toFixed(2)}

` + ${this.bookedPrice()}

` bookingDetail = `

Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your lesson is scheduled, or you will be charged for the lesson in full. @@ -378,7 +476,11 @@ UserStore = context.UserStore policies

` else - header = `

enter payment info

` + if @reuseStoredCard() + header = `

payment info already entered

` + else + header = `

enter payment info

` + bookingInfo = `

You are entering your credit card info so that later checkouts go quickly. You can skip this for now.

` bookingDetail = ` @@ -391,7 +493,7 @@ UserStore = context.UserStore

` - submitClassNames = {'button-orange': true, disabled: disabled && @state.updating} + submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating} updateCardClassNames = {'button-grey': true, disabled: disabled && @state.updating} backClassNames = {'button-grey': true, disabled: disabled && @state.updating} diff --git a/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee new file mode 100644 index 000000000..daa614670 --- /dev/null +++ b/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee @@ -0,0 +1,82 @@ +context = window + +@RescheduleLessonDialog = React.createClass({ + + mixins: [@PostProcessorMixin, Reflux.listenTo(@AppStore, "onAppInit")] + teacher: false + + beforeShow: (args) -> + logger.debug("RescheduleLessonDialog.beforeShow", args.d1) + + + @setState({id: args.d1, lesson_session: null}) + + rest.getLesson({id: args.d1}).done((response) => @getLessonDone(response)).fail((jqXHR) => @getLessonFail(jqXHR)) + + getLessonDone: (lesson_session) -> + @postProcessLesson(lesson_session) + @setState({lesson_session: lesson_session}) + + getLessonFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('reschedule-lesson-dialog', dialogBindings); + + componentDidMount: () -> + @root = $(@getDOMNode()) + + getInitialState: () -> + {id: null, lesson_session: null} + + onCloseClicked: (e) -> + e.preventDefault() + @app.layout.closeDialog('reschedule-lesson-dialog'); + + lesson: () -> + this.state.lesson_session + + viewerStudent: () -> + @lesson().student.id == context.JK.currentUserId + + viewerTeacher: () -> + !@viewerStudent() + + + render: () -> + + title = 'loading...' + if this.state.lesson_session? + title = "reschedule lesson" + lessonSessionId = this.state.lesson_session.id + other = this.state.lesson_session.other + + if @viewerStudent() && @sessionStartingSoon() + title = 'Policy Issue' + content = + `
+

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

+
+ CLOSE +
+
` + + `
+
+ + +

{title}

+
+
+ +
+
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee index b6ef5d193..2060193f9 100644 --- a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee @@ -19,12 +19,23 @@ UserStore = context.UserStore e.preventDefault() this.setState({clicked: true}) - StripeActions.connect(this.props.purpose) + StripeActions.connect(this.props.purpose, this.props.user) render: () -> + if this.props.purpose == 'jamclass-home' + label = `` + else + label = `` + if this.props.user?.stripe_auth? - return `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
` + if this.props.purpose == 'jamclass-home' + return `
+

Your Stripe account is properly set up and connected to enable transfer of student payments. To view lesson payment history, click the button below.

+ VIEW PAYMENTS +
` + else + return `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
` if this.state.clicked imageUrl = '/assets/content/stripe-connect-light-on-dark.png' @@ -32,7 +43,8 @@ UserStore = context.UserStore imageUrl = '/assets/content/stripe-connect-blue-on-dark.png' `
- + {label} +
` diff --git a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee index 28b9f2378..f21101986 100644 --- a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee @@ -8,14 +8,17 @@ logger = context.JK.logger componentDidMount: () -> @root = jQuery(this.getDOMNode()) - @root.off("submit", ".teacher-experience-teaching-form").on("submit", ".teacher-experience-teaching-form", @addExperience) + @root.off("submit", ".teacher-experience-teaching-form")#.on("submit", ".teacher-experience-teaching-form", @addExperience) formatListItem: (obj) -> if obj.end_year? endYear = obj.end_year + + if endYear == '0' || endYear == 0 + endYear = 'Now' else - endYear = 'Present' + endYear = 'Now' t = "#{obj.name}/#{obj.organization} (#{obj.start_year}" t += "-#{endYear}" if this.props.showEndDate @@ -30,12 +33,13 @@ logger = context.JK.logger addExperience: (e) -> e.preventDefault() - $form = e.target + $form = @root.find('.teacher-experience-teaching-form') - start_year = $("[name='start_year']", $form).val() - end_year = $("[name='end_year']", $form).val() + start_year = parseInt($("[name='start_year']", $form).val()) + end_year = parseInt($("[name='end_year']", $form).val()) - if this.props.showEndDate && start_year > end_year + console.log("end_yeaor", end_year) + if this.props.showEndDate && end_year != 0 && start_year > end_year this.setState({errors: ["End year must be greater than start year"]}) else this.props.listItems.push { @@ -48,7 +52,6 @@ logger = context.JK.logger this.props.onItemChanged(this.props.experienceType, this.props.listItems) #$form.reset() this.setState({errors: null}) - false getInitialState: () -> {errors:null} @@ -61,7 +64,7 @@ logger = context.JK.logger endDate = [] if this.props.showEndDate endDate.push ` - ` + ` dtLabel = "Start & End" else dtLabel = "Date" @@ -99,7 +102,7 @@ logger = context.JK.logger
- +
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 45861f89f..bed688d7b 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -27,6 +27,7 @@ proficiencyDescriptionMap = { @TeacherProfile = React.createClass({ mixins: [ + PostProcessorMixin, Reflux.listenTo(AppStore, "onAppInit"), Reflux.listenTo(UserStore, "onUserChanged"), Reflux.listenTo(SubjectStore, "onSubjectsChanged"), @@ -40,6 +41,7 @@ proficiencyDescriptionMap = { TILE_SAMPLES: 'samples' TILE_RATINGS: 'ratings' TILE_PRICES: 'prices' + visible: false TILES: ['about', 'experience', 'samples', 'ratings', 'prices'] @@ -60,11 +62,14 @@ proficiencyDescriptionMap = { componentDidMount: () -> @root = $(@getDOMNode()) + @screen = $('#teacher-profile') @starbox() componentDidUpdate:() -> @starbox() + context.JK.popExternalLinks(@root) + starbox:() -> $ratings = @root.find('.ratings-box') $ratings.each((i, value) => @@ -85,18 +90,31 @@ proficiencyDescriptionMap = { userDetailDone: (response) -> if response.id == @state.userId + console.log("teacher markup", response) + @postProcessUser(response) @setState({user: response, isSelf: response.id == context.JK.currentUserId}) else logger.debug("ignoring userDetailDone", response.id, @state.userId) + @showSideBubbleWhenReady() + + showSideBubbleWhenReady: () -> + if @user? && @state.user? + if @visible + @showSideBubble() + beforeHide: (e) -> + @visible = false + @hideSideBubble(); logger.debug("TeacherProfile: beforeHide") ProfileActions.viewTeacherProfileDone() beforeShow: (e) -> logger.debug("TeacherProfile: beforeShow") + afterShow: (e) -> + @visible = true logger.debug("TeacherProfile: afterShow") @setState({userId: e.id, user: null}) rest.getUserDetail({ @@ -105,6 +123,37 @@ proficiencyDescriptionMap = { show_profile: true }).done((response) => @userDetailDone(response)).fail(@app.ajaxError) + showSideBubble: () -> + # :remaining_test_drives, :can_buy_test_drive? + if @user['remaining_test_drives'] > 0 + @showUseRemainingTestDrivesBubble() + else if @user['can_buy_test_drive?'] + @showBuyTestDriveBubble() + else + @showBuyNormalLessonBubble() + + hideSideBubble: () -> + if @screen.btOff + @screen.btOff() + + showUseRemainingTestDrivesBubble: ( ) -> + context.JK.HelpBubbleHelper.showUseRemainingTestDrives(@screen, @screen, @user, (() => @useRemainingTestDrives())) + + showBuyTestDriveBubble: () -> + context.JK.HelpBubbleHelper.showBuyTestDrive(@screen, @screen, @user, (() => @buyTestDrive())) + + showBuyNormalLessonBubble: () -> + context.JK.HelpBubbleHelper.showBuyNormalLesson(@screen, @screen, @state.user, (() => @buyNormalLesson())) + + useRemainingTestDrives: () -> + window.location.href = "/client#/jamclass/book-lesson/test-drive_#{@state.user.id}" + buyTestDrive: () -> + window.location.href = "/client#/jamclass/test-drive-selection/#{@state.user.id}" + + + buyNormalLesson: () -> + window.location.href = "/client#/jamclass/book-lesson/normal_#{@state.user.id}" + getInitialState: () -> { userId: null, @@ -119,6 +168,7 @@ proficiencyDescriptionMap = { onUserChanged: (userState) -> @user = userState?.user + @showSideBubbleWhenReady() editProfile: (selected, e) -> e.preventDefault() @@ -176,15 +226,15 @@ proficiencyDescriptionMap = { embedId = @getYoutubeId(videoUrl) embedUrl = "//www.youtube.com/embed/#{embedId}" + video = ` + + ` + return `

Intro Video

-
-
-