diff --git a/admin/Gemfile b/admin/Gemfile index 2c28dbf41..c6218e01d 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -77,6 +77,9 @@ gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' gem 'recurly' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' group :libv8 do gem 'libv8', "~> 4.5.95" diff --git a/admin/app/admin/lesson_booking.rb b/admin/app/admin/lesson_booking.rb new file mode 100644 index 000000000..b30d6ca8a --- /dev/null +++ b/admin/app/admin/lesson_booking.rb @@ -0,0 +1,47 @@ +ActiveAdmin.register JamRuby::LessonBooking, :as => 'LessonBookings' do + + menu :label => 'LessonBooking', :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.unscoped.order('created_at desc') } + scope("Requested") { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended" ) { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled" ) { |scope| scope.unscoped.canceled.order('created_at desc') } + + index do + column "User Link" do |lesson_booking| + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + column "Type" do |lesson_booking| + lesson_booking.display_type + end + column "Status" do |lesson_booking| + lesson_booking.status + end + column "Teacher" do |lesson_booking| + teacher = lesson_booking.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_booking| + student = lesson_booking.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + end + + show do + + end + +end \ No newline at end of file diff --git a/admin/app/admin/lesson_session.rb b/admin/app/admin/lesson_session.rb new file mode 100644 index 000000000..c58eb4658 --- /dev/null +++ b/admin/app/admin/lesson_session.rb @@ -0,0 +1,59 @@ +ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do + + menu :label => 'LessonSession', :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.unscoped.order('created_at desc') } + scope("Requested" ) { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended" ) { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled" ) { |scope| scope.unscoped.canceled.order('created_at desc') } + scope("Missed" ) { |scope| scope.unscoped.missed.order('created_at desc') } + scope("Completed" ) { |scope| scope.unscoped.completed.order('created_at desc') } + + index do + column "User Link" do |lesson_session| + lesson_booking = lesson_session.lesson_booking + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + column "Status" do |lesson_session| + lesson_session.status + end + column "Start Time" do |lesson_session| + span do + lesson_session.music_session.pretty_scheduled_start(true) + end + br + span do + lesson_session.music_session.scheduled_start + end + end + column "Duration" do |lesson_session| + lesson_session.duration + end + column "Teacher" do |lesson_session| + teacher = lesson_session.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_session| + student = lesson_session.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + end + + show do + + end + +end \ No newline at end of file diff --git a/admin/app/admin/monthly_stats.rb b/admin/app/admin/monthly_stats.rb index b25990841..0a8e609bb 100644 --- a/admin/app/admin/monthly_stats.rb +++ b/admin/app/admin/monthly_stats.rb @@ -15,7 +15,6 @@ ActiveAdmin.register_page "Monthly Stats" do column "Sessions", :count end - h2 "Distinct Users Who Played with a JamTrack" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(user_id)) from jam_track_sessions jts group by month order by month desc;") do column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } @@ -36,5 +35,4 @@ ActiveAdmin.register_page "Monthly Stats" do end - end \ No newline at end of file diff --git a/admin/app/admin/students.rb b/admin/app/admin/students.rb index c9e91592f..a022aa727 100644 --- a/admin/app/admin/students.rb +++ b/admin/app/admin/students.rb @@ -1,4 +1,3 @@ -=begin ActiveAdmin.register JamRuby::User, :as => 'Students' do menu :label => 'Students', :parent => 'JamClass' @@ -9,66 +8,25 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do config.paginate = true def booked_anything(scope) - scope.joins(lesson_booking) + scope.joins(:student_lesson_bookings).where('lesson_bookings.active = true').uniq end - scope("Default", default: true) { |scope| booked_anything(scope) } + + scope("Default", default: true) { |scope| booked_anything(scope).order('ready_for_session_at IS NULL DESC') } index do - column "Name" do |teacher| - link_to teacher.user.name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" + column "Name" do |user| + link_to user.name, "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" end - column "Email" do |teacher| - teacher.user.email + column "Email" do |user| + user.email end - column "Location" do |teacher| - teacher.user.location(country = true) + column "Location" do |user| + user.location(country = true) end - column "Profile %" do |teacher| + + column "Session Ready" do |user| div do - span do - "#{teacher.pct_complete[:pct]}%" - end - br - span do - link_to "Detail", admin_teacher_path(teacher.id) - end - end - - end - column "Background Check" do |teacher| - div do - if teacher.background_check_at - span do - teacher.background_check_at.to_date - end - span do - br - end - span do - link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do - "mark as checked" - end - end - - else - span do - 'NOT DONE' - end - span do - br - end - span do - link_to("mark as checked", mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) - end - end - - - end - end - - column "Session Ready" do |teacher| - div do - if teacher.ready_for_session + if user.ready_for_session_at span do 'YES' end @@ -80,166 +38,22 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do br end span do - link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) + link_to("mark as checked", mark_session_ready_admin_student_path(user.id), {confirm: "Mark as ready for session?"}) end end end end - column "Top Teacher" do |teacher| - div do - if teacher.top_rated - span do - 'YES' - end - span do - br - end - span do - link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) - end - end - end - - end - end - - show do - attributes_table do - row "Name" do |teacher| - link_to teacher.user.name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" - end - row "Email" do |teacher| - teacher.user.email - end - row "Location" do |teacher| - teacher.user.location(country = true) - end - row "Profile %" do |teacher| - div do - span do - "#{teacher.pct_complete[:pct]}%" - end - br - br - div do - h5 do "Completed Sections" end - teacher.pct_complete.each do |k, v| - if k != :pct && v - div do - k - end - end - end - br - br - h5 do "Uncompleted Sections" end - teacher.pct_complete.each do |k, v| - if k != :pct && !v - div do - k - end - end - end - end - end - - end - row "Background Check" do |teacher| - div do - if teacher.background_check_at - span do - teacher.background_check_at.to_date - end - span do - br - end - span do - link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do - "mark as checked" - end - end - - else - span do - 'NOT DONE' - end - span do - br - end - span do - link_to("mark as checked", mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) - end - end - - - end - end - - row "Session Ready" do |teacher| - div do - if teacher.ready_for_session - span do - 'YES' - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) - end - - end - end - end - row "Top Teacher" do |teacher| - div do - if teacher.top_rated - span do - 'YES' - end - span do - br - end - span do - link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) - end - end - end - + column "School" do |user| + if teacher.school + teacher.school.name end end end - member_action :mark_session_ready, :method => :get do resource.mark_session_ready redirect_to :back end -end -=end \ No newline at end of file +end \ No newline at end of file diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb index 9ffa7e3d9..942e501d5 100644 --- a/admin/app/admin/teachers.rb +++ b/admin/app/admin/teachers.rb @@ -45,6 +45,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=begin column "Background Check" do |teacher| div do if teacher.background_check_at @@ -73,6 +74,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end end +=end column "Session Ready" do |teacher| div do if teacher.ready_for_session_at @@ -122,7 +124,11 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do column "Signed Up" do |teacher| teacher.created_at.to_date end - + column "School" do |teacher| + if teacher.school + teacher.school.name + end + end end show do @@ -166,6 +172,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=begin row "Background Check" do |teacher| div do if teacher.background_check_at @@ -196,6 +203,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=end row "Session Ready" do |teacher| div do @@ -248,6 +256,12 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do teacher.created_at.to_date end + row "School" do |teacher| + if teacher.school + teacher.school.name + end + end + end end diff --git a/admin/app/controllers/application_controller.rb b/admin/app/controllers/application_controller.rb index 15e327c98..d3e00deb6 100644 --- a/admin/app/controllers/application_controller.rb +++ b/admin/app/controllers/application_controller.rb @@ -1,4 +1,6 @@ class ApplicationController < ActionController::Base + include ApplicationHelper + protect_from_forgery before_filter :prepare_gon diff --git a/admin/app/helpers/application_helper.rb b/admin/app/helpers/application_helper.rb index 6e9385e59..80731359d 100644 --- a/admin/app/helpers/application_helper.rb +++ b/admin/app/helpers/application_helper.rb @@ -1,4 +1,5 @@ module ApplicationHelper + end diff --git a/db/manifest b/db/manifest index 9c07236f3..4067937f3 100755 --- a/db/manifest +++ b/db/manifest @@ -340,4 +340,5 @@ jamblaster_pairing_active.sql email_blacklist.sql jamblaster_connection.sql teacher_progression.sql -teacher_complete.sql \ No newline at end of file +teacher_complete.sql +lessons.sql \ No newline at end of file diff --git a/db/up/lessons.sql b/db/up/lessons.sql new file mode 100644 index 000000000..bc6ff6daf --- /dev/null +++ b/db/up/lessons.sql @@ -0,0 +1,261 @@ + +CREATE TABLE lesson_package_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR NOT NULL, + description VARCHAR NOT NULL, + package_type VARCHAR(64) NOT NULL, + price NUMERIC(8,2), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_bookings ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + active BOOLEAN NOT NULL DEFAULT FALSE, + accepter_id VARCHAR(64) REFERENCES users(id), + canceler_id VARCHAR(64) REFERENCES users(id), + lesson_type VARCHAR(64) NOT NULL, + recurring BOOLEAN NOT NULL, + lesson_length INTEGER NOT NULL, + payment_style VARCHAR(64) NOT NULL, + description VARCHAR, + booked_price NUMERIC(8,2) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + card_presumed_ok BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + status VARCHAR, + cancel_message VARCHAR, + user_decremented BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE charges ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL DEFAULT 0, + type VARCHAR(64) NOT NULL, + sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_billing_notices_at TIMESTAMP, + last_billing_attempt_at TIMESTAMP, + billed BOOLEAN NOT NULL DEFAULT FALSE, + billed_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + billing_error_reason VARCHAR, + billing_error_detail VARCHAR, + billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE , + billing_attempts INTEGER NOT NULL DEFAULT 0, + stripe_charge_id VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_package_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id) NOT NULL, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id), + price NUMERIC(8,2), + recurring BOOLEAN NOT NULL DEFAULT FALSE, + year INTEGER, + month INTEGER, + charge_id VARCHAR(64) REFERENCES charges(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE lesson_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_type VARCHAR(64) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + duration INTEGER NOT NULL, + booked_price NUMERIC(8,2) NOT NULL, + teacher_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled BOOLEAN DEFAULT FALSE NOT NULL, + teacher_canceled BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled_at TIMESTAMP, + teacher_canceled_at TIMESTAMP, + student_canceled_reason VARCHAR, + teacher_canceled_reason VARCHAR, + status VARCHAR, + analysed BOOLEAN NOT NULL DEFAULT FALSE, + analysis JSON, + analysed_at TIMESTAMP, + cancel_message VARCHAR, + canceler_id VARCHAR(64) REFERENCES users(id), + charge_id VARCHAR(64) REFERENCES charges(id), + success BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200); +ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN; + +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive', 'Test Drive', 'Four reduced-price lessons which you can use to find that ideal teacher.', 'test-drive', 49.99); + + +CREATE TABLE lesson_booking_slots ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + slot_type VARCHAR(64) NOT NULL, + preferred_day DATE, + day_of_week INTEGER, + hour INTEGER, + minute INTEGER, + timezone VARCHAR NOT NULL, + message VARCHAR, + accept_message VARCHAR, + update_all BOOLEAN NOT NULL DEFAULT FALSE, + proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_bookings ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); + +ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id); +ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id); +ALTER TABLE users ADD COLUMN remaining_free_lessons INTEGER NOT NULL DEFAULT 1; +ALTER TABLE users ADD COLUMN stored_credit_card BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN remaining_test_drives INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN stripe_token VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_zip_code VARCHAR(200); +ALTER TABLE sales ADD COLUMN stripe_charge_id VARCHAR(200); +ALTER TABLE teachers ADD COLUMN stripe_account_id VARCHAR(200); +ALTER TABLE sale_line_items ADD COLUMN lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id); + + +-- one is created every time the teacher is paid. N teacher_distributions point to this +CREATE TABLE teacher_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + charge_id VARCHAR(64) REFERENCES charges(id) NOT NULL, + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- one is created for every bit of money the teacher is due +CREATE TABLE teacher_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_payment_id VARCHAR(64) REFERENCES teacher_payments(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + amount_in_cents INTEGER NOT NULL, + ready BOOLEAN NOT NULL DEFAULT FALSE, + distributed BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE affiliate_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) NOT NULL, + affiliate_referral_fee_in_cents INTEGER NOT NULL, + sale_line_item_id VARCHAR(64) REFERENCES sale_line_items(id) NOT NULL, + affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE, + affiliate_refunded_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE affiliate_partners ADD COLUMN lesson_rate NUMERIC (8,2) NOT NULL DEFAULT 0.20; + +-- move over all sale_line_item affiliate info +INSERT INTO affiliate_distributions ( + SELECT + sale_line_items.id, + sale_line_items.affiliate_referral_id, + sale_line_items.affiliate_referral_fee_in_cents, + sale_line_items.id, + sale_line_items.affiliate_refunded, + sale_line_items.affiliate_refunded_at, + sale_line_items.created_at, + sale_line_items.updated_at + FROM sale_line_items + WHERE sale_line_items.affiliate_referral_id IS NOT NULL +); + +CREATE TABLE teacher_intents ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES teachers(id) NOT NULL, + intent VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX teacher_intents_intent_idx ON teacher_intents(teacher_id, intent); + +CREATE TABLE schools ( + id INTEGER PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + name VARCHAR, + enabled BOOLEAN DEFAULT TRUE, + scheduling_communication VARCHAR NOT NULL DEFAULT 'teacher', + correspondence_email VARCHAR, + photo_url VARCHAR(2048), + original_fpfile VARCHAR(8000), + cropped_fpfile VARCHAR(8000), + cropped_s3_path VARCHAR(8000), + crop_selection VARCHAR(256), + large_photo_url VARCHAR(512), + cropped_large_s3_path VARCHAR(512), + cropped_large_fpfile VARCHAR(8000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE SEQUENCE school_key_sequence; +ALTER SEQUENCE school_key_sequence RESTART WITH 10000; +ALTER TABLE schools ALTER COLUMN id SET DEFAULT nextval('school_key_sequence'); + +ALTER TABLE users ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE users ADD COLUMN joined_school_at TIMESTAMP; +ALTER TABLE teachers ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teachers ADD COLUMN joined_school_at TIMESTAMP; + +CREATE TABLE school_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + school_id INTEGER REFERENCES schools(id) NOT NULL, + invitation_code VARCHAR(256) NOT NULL UNIQUE, + note VARCHAR, + as_teacher BOOLEAN NOT NULL, + email VARCHAR NOT NULL, + first_name VARCHAR, + last_name VARCHAR, + accepted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE teachers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; +ALTER TABLE schools ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 1817f2241..57799fc7b 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -92,6 +92,9 @@ message ClientMessage { MIXDOWN_SIGN_COMPLETE = 270; MIXDOWN_SIGN_FAILED = 271; + LESSON_MESSAGE = 280; + SCHEDULED_JAMCLASS_INVITATION = 281; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -211,6 +214,9 @@ message ClientMessage { optional MixdownSignComplete mixdown_sign_complete = 270; optional MixdownSignFailed mixdown_sign_failed = 271; + // lesson notifications + optional LessonMessage lesson_message = 280; + optional ScheduledJamclassInvitation scheduled_jamclass_invitation = 281; // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -678,6 +684,31 @@ message MixdownSignFailed { required string mixdown_package_id = 1; // jam track mixdown package id } +message LessonMessage { + optional string music_session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; + optional string sender_id = 6; + optional string receiver_id = 7; + optional bool student_directed = 8; + optional string purpose = 9; + optional string sender_name = 10; + optional string lesson_session_id = 11; +} + +message ScheduledJamclassInvitation { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string session_name = 4; + optional string session_date = 5; + optional string notification_id = 6; + optional string created_at = 7; + optional string lesson_session_id = 8; +} + message SubscriptionMessage { optional string type = 1; // the type of the subscription diff --git a/ruby/Gemfile b/ruby/Gemfile index 0680c23cb..5c4c66424 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -52,6 +52,10 @@ gem 'sanitize' gem 'influxdb', '0.1.8' gem 'recurly' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'icalendar' +gem 'email_validator' group :test do gem 'simplecov', '~> 0.7.1' @@ -66,7 +70,8 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' - gem 'icalendar' + gem 'stripe-ruby-mock' + end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index d301a7cea..2fddb85f5 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -21,6 +21,9 @@ require 'rest-client' require 'zip' require 'csv' require 'tzinfo' +require 'stripe' +require 'zip-codes' +require 'email_validator' require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" @@ -53,6 +56,7 @@ require "jam_ruby/resque/scheduled/cleanup_facebook_signup" 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/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_reminder" @@ -268,10 +272,27 @@ require "jam_ruby/models/gift_card" require "jam_ruby/models/gift_card_purchase" require "jam_ruby/models/gift_card_type" require "jam_ruby/models/jam_track_session" +require "jam_ruby/models/lesson_package_type" +require "jam_ruby/models/lesson_package_purchase" +require "jam_ruby/models/lesson_session" +require "jam_ruby/models/lesson_booking" +require "jam_ruby/models/lesson_booking_slot" require "jam_ruby/models/jamblaster" require "jam_ruby/models/jamblaster_user" require "jam_ruby/models/jamblaster_pairing_request" require "jam_ruby/models/sale_receipt_ios" +require "jam_ruby/models/lesson_session_analyser" +require "jam_ruby/models/lesson_session_monthly_price" +require "jam_ruby/models/teacher_distribution" +require "jam_ruby/models/teacher_payment" +require "jam_ruby/models/charge" +require "jam_ruby/models/teacher_payment_charge" +require "jam_ruby/models/affiliate_payment_charge" +require "jam_ruby/models/lesson_payment_charge" +require "jam_ruby/models/affiliate_distribution" +require "jam_ruby/models/teacher_intent" +require "jam_ruby/models/school" +require "jam_ruby/models/school_invitation" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 40d5480f0..1612f140f 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -1,606 +1,1487 @@ - module JamRuby - # UserMailer must be configured to work - # Some common configs occur in jam_ruby/init.rb - # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), - # and in config/initializers/email.rb in rails to configure sendmail account settings - # If UserMailer were to be used in another project, it would need to be configured there, as well. +module JamRuby + # UserMailer must be configured to work + # Some common configs occur in jam_ruby/init.rb + # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), + # and in config/initializers/email.rb in rails to configure sendmail account settings + # If UserMailer were to be used in another project, it would need to be configured there, as well. - # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer - class UserMailer < ActionMailer::Base - include SendGrid + # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer + class UserMailer < ActionMailer::Base + include SendGrid - layout "user_mailer" + layout "user_mailer" - DEFAULT_SENDER = "JamKazam " + DEFAULT_SENDER = "JamKazam " - default :from => DEFAULT_SENDER + default :from => DEFAULT_SENDER - sendgrid_category :use_subject_lines - #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) - sendgrid_unique_args :env => Environment.mode + sendgrid_category :use_subject_lines + #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) + sendgrid_unique_args :env => Environment.mode - def confirm_email(user, signup_confirm_url) - @user = user - @signup_confirm_url = signup_confirm_url - sendgrid_category "Confirm Email" - sendgrid_unique_args :type => "confirm_email" + def confirm_email(user, signup_confirm_url) + @user = user + @signup_confirm_url = signup_confirm_url + sendgrid_category "Confirm Email" + sendgrid_unique_args :type => "confirm_email" - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) - mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| - format.text - format.html - end + mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| + format.text + format.html + end + end + + def welcome_message(user) + @user = user + 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 => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def student_welcome_message(user) + @user = user + 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 => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def teacher_welcome_message(user) + @user = user + 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 => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def password_changed(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "password_changed" + mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| + format.text + format.html + end + end + + def password_reset(user, password_reset_url) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + @password_reset_url = password_reset_url + sendgrid_unique_args :type => "password_reset" + mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| + format.text + format.html + end + end + + def updating_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updating_email" + mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| + format.text + format.html + end + end + + def updated_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updated_email" + mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| + format.text + format.html + end + end + + def new_musicians(user, new_musicians, host='www.jamkazam.com') + @user, @new_musicians, @host = user, new_musicians, host + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_unique_args :type => "new_musicians" + + mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| + format.text + format.html + end + end + + #################################### NOTIFICATION EMAILS #################################### + def friend_request(user, msg, friend_request_id) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend request on JamKazam" + unique_args = {:type => "friend_request"} + + @url = Nav.accept_friend_request_dialog(friend_request_id) + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def friend_request_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend on JamKazam" + unique_args = {:type => "friend_request_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_user_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new follower on JamKazam" + unique_args = {:type => "new_user_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_band_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band has a new follower on JamKazam" + unique_args = {:type => "new_band_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def session_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to a session on JamKazam" + unique_args = {:type => "session_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "Someone you know is in a session on JamKazam" + unique_args = {:type => "musician_session_join"} + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_invitation(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Invitation" + unique_args = {:type => "scheduled_session_invitation"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP" + unique_args = {:type => "scheduled_session_rsvp"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_approved(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Approved" + unique_args = {:type => "scheduled_session_rsvp_approved"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled_org(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Your Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Cancelled" + unique_args = {:type => "scheduled_session_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rescheduled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Rescheduled" + unique_args = {:type => "scheduled_session_rescheduled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_reminder_upcoming(user, session) + subject = "Your JamKazam session starts in 1 hour!" + unique_args = {:type => "scheduled_session_reminder_upcoming"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def scheduled_session_reminder_day(user, session) + subject = "JamKazam Session Reminder" + unique_args = {:type => "scheduled_session_reminder_day"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def send_scheduled_session_reminder(user, session, subject, unique_args) + return if !user.subscribe_email + + email = user.email + @user = user + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_comment(target_user, sender, msg, comment, session) + return if !target_user.subscribe_email + + email = target_user.email + subject = "New Session Comment" + unique_args = {:type => "scheduled_session_comment"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @comment = comment + @sender = sender + @suppress_user_has_account_footer = true + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [target_user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def scheduled_session_daily(receiver, sessions_and_latency) + sendgrid_category "Notification" + sendgrid_unique_args :type => "scheduled_session_daily" + + sendgrid_recipients([receiver.email]) + sendgrid_substitute('@USERID', [receiver.id]) + + @user = receiver + @sessions_and_latency = sessions_and_latency + + @title = 'New Scheduled Sessions Matched to You' + mail(:to => receiver.email, + :subject => EmailBatchScheduledSessions.subject) do |format| + format.text + format.html + end + end + + def band_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "A band that you follow has joined a session" + unique_args = {:type => "band_session_join"} + + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A musician has saved a new recording on JamKazam" + unique_args = {:type => "musician_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A band has saved a new recording on JamKazam" + unique_args = {:type => "band_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to join a band on JamKazam" + unique_args = {:type => "band_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band invitation was accepted" + unique_args = {:type => "band_invitation_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + + def text_message(user, sender_id, sender_name, sender_photo_url, message) + return if !user.subscribe_email + + email = user.email + subject = "Message from #{sender_name}" + unique_args = {:type => "text_message"} + + @note = message + @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) + @sender_id = sender_id + @sender_name = sender_name + @sender_photo_url = sender_photo_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_request(lesson_booking) + email = lesson_booking.user.email + subject = "You have sent a lesson request to #{lesson_booking.teacher.name}!" + unique_args = {:type => "student_lesson_request"} + + @sender = lesson_booking.teacher + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_booking.user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_request(lesson_booking) + email = lesson_booking.teacher.email + subject = "You have received a lesson request through JamKazam!" + unique_args = {:type => "teacher_lesson_request"} + + @sender = lesson_booking.user + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_booking.teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "Your have confirmed a lesson!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "Your lesson request is confirmed!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "Your lesson time change is confirmed by #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "You have confirmed a lesson!" end - def welcome_message(user) - @user = user - sendgrid_category "Welcome" - sendgrid_unique_args :type => "welcome_message" + @lesson_session = lesson_session + @message = message + email = lesson_session.teacher.email + unique_args = {:type => "teacher_lesson_accepted"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.student.name}!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.student.name}!" end - def password_changed(user) - @user = user + @lesson_session = lesson_session + @message = message + email = lesson_session.teacher.email + unique_args = {:type => "teacher_lesson_update_all"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "password_changed" - mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def password_reset(user, password_reset_url) - @user = user + def teacher_scheduled_jamclass_invitation(user, msg, session) - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.student.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @password_reset_url = password_reset_url - sendgrid_unique_args :type => "password_reset" - mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def updating_email(user) - @user = user + def student_scheduled_jamclass_invitation(user, msg, session) + return if !user.subscribe_email - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.teacher.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updating_email" - mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def updated_email(user) - @user = user + # teacher proposed counter time; so send msg to the student + def student_lesson_counter(lesson_session, slot) - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = lesson_session.student.email + subject = "Instructor has proposed a different time for your lesson" + unique_args = {:type => "student_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updated_email" - mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| - format.text - format.html - end + 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 - def new_musicians(user, new_musicians, host='www.jamkazam.com') - @user, @new_musicians, @host = user, new_musicians, host - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_unique_args :type => "new_musicians" + # student proposed counter time; so send msg to the teacher + def teacher_lesson_counter(lesson_session, slot) - mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| - format.text - format.html - end + email = lesson_session.teacher.email + subject = "Student has proposed a different time for their lesson" + unique_args = {:type => "teacher_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + 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 - #################################### NOTIFICATION EMAILS #################################### - def friend_request(user, msg, friend_request_id) - return if !user.subscribe_email + def teacher_lesson_completed(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 = lesson_session.teacher.email + subject = "You successfully completed a lesson with #{@student.name}" + unique_args = {:type => "teacher_lesson_completed"} - email = user.email - subject = "You have a new friend request on JamKazam" - unique_args = {:type => "friend_request"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @url = Nav.accept_friend_request_dialog(friend_request_id) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def friend_request_accepted(user, msg) - return if !user.subscribe_email + # successfully completed, and has some remaining test drives + def student_test_drive_lesson_completed(lesson_session) - email = user.email - subject = "You have a new friend on JamKazam" - unique_args = {:type => "friend_request_accepted"} + @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 - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + 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 - def new_user_follower(user, msg) - return if !user.subscribe_email + # successfully completed, but no more test drives left + def student_test_drive_lesson_done(lesson_session) - email = user.email - subject = "You have a new follower on JamKazam" - unique_args = {:type => "new_user_follower"} + @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 - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "You have used all 4 TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "raw_mailer" } end + end - def new_band_follower(user, msg) - return if !user.subscribe_email + def student_lesson_normal_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 = user.email - subject = "Your band has a new follower on JamKazam" - unique_args = {:type => "new_band_follower"} + email = @student.email + subject = "Your lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_no_bill"} - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def session_invitation(user, msg) - return if !user.subscribe_email + def teacher_lesson_normal_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 = lesson_session.teacher.email + subject = "Your student #{@student.name} will not be charged for their lesson" + unique_args = {:type => "teacher_lesson_normal_no_bill"} - email = user.email - subject = "You have been invited to a session on JamKazam" - unique_args = {:type => "session_invitation"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def musician_session_join(user, msg, session_id) - return if !user.subscribe_email + def student_lesson_normal_done(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 = user.email - subject = "Someone you know is in a session on JamKazam" - unique_args = {:type => "musician_session_join"} - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "Your JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "student_lesson_normal_done"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + 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 - def scheduled_session_invitation(user, msg, session) - return if !user.subscribe_email + def teacher_lesson_normal_done(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 = lesson_session.teacher.email + subject = "Your JamClass lesson today with #{@student.first_name}" + unique_args = {:type => "teacher_lesson_normal_done"} - email = user.email - subject = "Session Invitation" - unique_args = {:type => "scheduled_session_invitation"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp(user, msg, session) - return if !user.subscribe_email + def student_unable_charge(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 + @card_declined = lesson_session.is_card_declined? + @card_expired = lesson_session.is_card_expired? + @bill_date = lesson_session.last_billed_at_date - email = user.email - subject = "Session RSVP" - unique_args = {:type => "scheduled_session_rsvp"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "student_unable_charge"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + 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 - def scheduled_session_rsvp_approved(user, msg, session) - return if !user.subscribe_email + def teacher_unable_charge(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 = lesson_session.teacher.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "teacher_lesson_normal_done"} - email = user.email - subject = "Session RSVP Approved" - unique_args = {:type => "scheduled_session_rsvp_approved"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp_cancelled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_rsvp_cancelled_org(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Your Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_cancelled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session Cancelled" - unique_args = {:type => "scheduled_session_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_rescheduled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session Rescheduled" - unique_args = {:type => "scheduled_session_rescheduled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_reminder_upcoming(user, session) - subject = "Your JamKazam session starts in 1 hour!" - unique_args = {:type => "scheduled_session_reminder_upcoming"} - send_scheduled_session_reminder(user, session, subject, unique_args) - end - - def scheduled_session_reminder_day(user, session) - subject = "JamKazam Session Reminder" - unique_args = {:type => "scheduled_session_reminder_day"} - send_scheduled_session_reminder(user, session, subject, unique_args) - end - - def send_scheduled_session_reminder(user, session, subject, unique_args) - return if !user.subscribe_email - - email = user.email - @user = user - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_comment(target_user, sender, msg, comment, session) - return if !target_user.subscribe_email - - email = target_user.email - subject = "New Session Comment" - unique_args = {:type => "scheduled_session_comment"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @comment = comment - @sender = sender - @suppress_user_has_account_footer = true - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [target_user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end - end - - def scheduled_session_daily(receiver, sessions_and_latency) - sendgrid_category "Notification" - sendgrid_unique_args :type => "scheduled_session_daily" - - sendgrid_recipients([receiver.email]) - sendgrid_substitute('@USERID', [receiver.id]) - - @user = receiver - @sessions_and_latency = sessions_and_latency - - @title = 'New Scheduled Sessions Matched to You' - mail(:to => receiver.email, - :subject => EmailBatchScheduledSessions.subject) do |format| - format.text - format.html - end - end - - def band_session_join(user, msg, session_id) - return if !user.subscribe_email - - email = user.email - subject = "A band that you follow has joined a session" - unique_args = {:type => "band_session_join"} - - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def musician_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A musician has saved a new recording on JamKazam" - unique_args = {:type => "musician_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A band has saved a new recording on JamKazam" - unique_args = {:type => "band_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "You have been invited to join a band on JamKazam" - unique_args = {:type => "band_invitation"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation_accepted(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "Your band invitation was accepted" - unique_args = {:type => "band_invitation_accepted"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + def student_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@teacher.name} have been suspended." + else + @subject = "The credit card charge for your #{@month_name} lessons with #{@teacher.name} failed." end - def text_message(user, sender_id, sender_name, sender_photo_url, message) - return if !user.subscribe_email + unique_args = {:type => "student_unable_charge_monthly"} - email = user.email - subject = "Message from #{sender_name}" - unique_args = {:type => "text_message"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @note = message - @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) - @sender_id = sender_id - @sender_name = sender_name - @sender_photo_url = sender_photo_url - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end + def teacher_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = @teacher.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." end - # def send_notification(email, subject, msg, unique_args) - # @body = msg - # sendgrid_category "Notification" - # sendgrid_unique_args :type => unique_args[:type] - # mail(:bcc => email, :subject => subject) do |format| - # format.text - # format.html - # end - # end - ############################################################################################# + unique_args = {:type => "teacher_unable_charge_monthly"} + + 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 student_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + @subject = "Your JamClass lessons with #{@teacher.first_name} for #{@month_name}" + + unique_args = {:type => "student_lesson_monthly_charged"} + + 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 + + def teacher_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = @teacher.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." + end + + + unique_args = {:type => "student_lesson_monthly_charged"} + + 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 teacher_distribution_done(teacher_payment) + @teacher_payment = teacher_payment + @teacher = teacher_payment.teacher + email = @teacher.email + + @subject = "You have received payment for your participation in JamClass" + unique_args = {:type => "teacher_distribution_done"} + + 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 + end + end + + def teacher_distribution_fail(teacher_payment) + @teacher_payment = teacher_payment + @teacher = teacher_payment.teacher + email = @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" + unique_args = {:type => "teacher_distribution_fail"} + + 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 + end + end + + def monthly_recurring_done(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 JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "student_lesson_normal_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 + + def monthly_recurring_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 lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_done"} + + 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 + + def student_lesson_booking_declined(lesson_booking, message) + @lesson_booking = 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 = "We're sorry your lesson request has been declined" + unique_args = {:type => "student_lesson_booking_declined"} + + 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 + + def student_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = 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 has been canceled" + unique_args = {:type => "student_lesson_booking_canceled"} + + 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 + + def teacher_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = 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 = @teacher.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_booking_canceled"} + + 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 student_lesson_canceled(lesson_session, message) + @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 has been canceled" + unique_args = {:type => "student_lesson_canceled"} + + 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 + + def teacher_lesson_canceled(lesson_session, message) + @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 = @teacher.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_canceled"} + + 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 invite_school_teacher(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school + + email = school_invitation.email + @subject = "#{@school.owner.name} has sent you an invitation to join #{@school.name} on JamKazam" + unique_args = {:type => "invite_school_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def invite_school_student(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school + + email = school_invitation.email + @subject = "#{@school.name} has sent you an invitation to join JamKazam for lessons" + unique_args = {:type => "invite_school_student"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end end end +end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb new file mode 100644 index 000000000..164315809 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +

<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can + register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the SIGN UP NOW + button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of + <%= @school.name %>, welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb new file mode 100644 index 000000000..1b1ea765b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can +register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the link +below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of +<%= @school.name %>, welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb new file mode 100644 index 000000000..6e59f21d8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +
+

+ <%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music + lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below, + and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+ +
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb new file mode 100644 index 000000000..c31475ea8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music +lessons in an amazing new way that really works. To accept this invitation, please click the link below, +and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards, +Team JamKazam \ No newline at end of file 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 new file mode 100644 index 000000000..96bf19889 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

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

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb new file mode 100644 index 000000000..96bf19889 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

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

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb new file mode 100644 index 000000000..3dc58d8ec --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. +
+
+ 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/monthly_recurring_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb new file mode 100644 index 000000000..f21f164b5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. + +To see this lesson, click here: <%= @lesson_session.web_url %> 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 new file mode 100644 index 000000000..d60afb1dc --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + This teacher has accepted your lesson request! + <% else %> + You have confirmed a lesson request. + <% end %> + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @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 +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..bc819bb5e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @teacher.name %> have been canceled. + + <% else %> + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

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

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb new file mode 100644 index 000000000..410621edb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ + This teacher has declined your lesson request. + + <% if @message.present? %> +

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

Click the button below to view the teacher's response.

+

+ VIEW RESPONSE +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb new file mode 100644 index 000000000..484bccb78 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

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

Click the button below to view more info about the canceled session.

+

+ VIEW RESPONSE +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb new file mode 100644 index 000000000..38ec8a404 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@teacher.name} has proposed a different time for your lesson") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @teacher.name %> has proposed a different time for your lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb new file mode 100644 index 000000000..67972c906 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @teacher.name %> has proposed a different time for your lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb new file mode 100644 index 000000000..d795f64b4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..7328cf9a7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. + +<% if !@student.has_rated_teacher(@teacher) %> +If you haven't already done so, please rate your teachernow to help other students in the community find the best instructors. <%= @teacher.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. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb new file mode 100644 index 000000000..d795f64b4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + 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 new file mode 100644 index 000000000..1a3c7e42a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

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

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb new file mode 100644 index 000000000..efdccf098 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb @@ -0,0 +1,16 @@ +Hello <%= @student.name %>, + +We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson. + +<% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. You can rate your teacher here: <%= @teacher.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. + +Best Regards, +Team JamKazam + + 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 new file mode 100644 index 000000000..f7836f6ce --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

You will not be billed for today's session 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_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..0c6d60a65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.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_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb new file mode 100644 index 000000000..4636052b0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson requested of #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

You have requested a <%= @lesson_booking.display_type %> lesson.

Click the button below to see your lesson request. You will receive another email when the teacher accepts or rejects the request.

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb new file mode 100644 index 000000000..9ec7f863e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb @@ -0,0 +1,3 @@ +You have requested a lesson from <%= @sender.name %>. + +To see this lesson request, click here: <%= @lesson_booking.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb new file mode 100644 index 000000000..d0259e567 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.teacher.name %> have been rescheduled. + + <% if @message.present? %> +

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

Click the button below to get more information and to update 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 +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb new file mode 100644 index 000000000..6011f7e65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.teacher.name%> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file 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 new file mode 100644 index 000000000..67590256e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ 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 new file mode 100644 index 000000000..c0e1bcb4d --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -0,0 +1,30 @@ +<% provide(:title, "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have + used <%= @student.used_test_drives %> TestDrive credits, and you have <%= @student.remaining_test_drives %> + remaining TestDrive lesson(s) available. If you haven’t booked your next TestDrive lesson, + click here to search our teachers and get your next + lesson lined up today!

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + Also, please rate your teacher now for today’s lesson + to help other students in the community find the best instructors. + <% end %> + And 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + 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 new file mode 100644 index 000000000..37ea61020 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -0,0 +1,14 @@ +You have used <%= @student.remaining_test_drives %> of 4 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. +<% end %> + +If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the +link to your favorite to book single or weekly recurring lessons with the best instructor for you! +<% @student.recent_test_drive_teachers.each do |teacher| %> +<%= teacher.name %>: <%= teacher.teacher_profile_url %> +<% end %> + +And 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. 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 new file mode 100644 index 000000000..b5e14e612 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb @@ -0,0 +1,47 @@ +<% provide(:title, "You have used all 4 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. + + <% if !@student.has_rated_teacher(@teacher) %> + Please rate your teacher now for today’s lesson to + help other students in the community find the best instructors. + <% end %> +

+

+ If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the + link to your favorite to book single or weekly recurring lessons with the best instructor for you! +

+<% @student.recent_test_drive_teachers.each do |teacher| %> + + + + + + +
+

+ +

+ <%= teacher.name %> +

+
+ +<% end %> + +

+ And 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.

+
+ +

+ Best Regards,
Team JamKazam +

+ + + 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 new file mode 100644 index 000000000..2d4d02829 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb @@ -0,0 +1,8 @@ +You have used all of your 4 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. +<% end %> + +And 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. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb new file mode 100644 index 000000000..2da383332 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "The credit card charge for your lesson today with #{@teacher.first_name} failed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ + <% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> + +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb new file mode 100644 index 000000000..b4cfb4628 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb @@ -0,0 +1,18 @@ +The credit card charge for your lesson today with <%= @teacher.first_name %> failed. +Hello <%= @student.name %>, + +<% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_session.update_payment_url %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb new file mode 100644 index 000000000..f4d5f5402 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb @@ -0,0 +1,32 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +
+
+ <% end %> + + <% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb new file mode 100644 index 000000000..2567d6891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb @@ -0,0 +1,23 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +<% end %> + +<% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_booking.update_payment_url %> + +Best Regards, +Team JamKazam + + 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 new file mode 100644 index 000000000..177f38c92 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -0,0 +1,119 @@ +<% provide(:title, 'Welcome to JamKazam!') %> + + +<% if !@user.anonymous? %> +

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

+<% end %> + + +

We're delighted you have decided to join the JamKazam community of musicians, and we hope + + you will enjoy using JamKazam and JamTracks to play more music. Following are some + + resources and some things you can do to get the most out of JamKazam. +

+ + +

Playing With JamTracks
+ + JamTracks are the best way to play along with your favorite songs. Far better and different than + + traditional backing tracks, our JamTracks are complete multi-track professional recordings, with + + fully isolated tracks for each part of the music. And our free app and Internet service are packed + + with features that give you unmatched creative freedom to learn, practice, record, play with + + others, and share your performances. Here are some great JamTracks resources: +

+ + +

Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over + + the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio + + quality. You can set up online sessions that are public or private, for you alone or for others to + + join. You can find and join others’ sessions, use backing tracks and loops in sessions, make + + audio and video recordings of session performances, and more. Click here for a set of tutorial + + videos that show how to use these features. +

+ +

Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how + + unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and + + student playing together at all. JamKazam is a terrific service for teaching and taking online + + music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and + + your students in getting set up and ready to go. +

+ +

Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who + + you are as a musician, as well as your musical interests. For example, what instruments do you + + play? What musical genres do you like best? Are you interested in getting into a virtual/online + + or a real-world band? And so on. Filling out your profile will help you connect with others with + + common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then + + click on the Profile tile, and click the Edit Profile button. +

+ +

Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do + + this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the + + user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in + + this menu, and you can then use the options to invite your friends using their email addresses, + + or via Facebook, or using your Google contacts. +

+ +

Get Help
+ + If you run into trouble and need help, please reach out to us. We will be glad to do everything + + we can to answer your questions and get you up and running. You can visit our Support Portal + + to find knowledge base articles and post questions that have not already been answered. You + + can email us at support@jamkazam.com. And if you just want to chat, share tips and war + + stories, and hang out with fellow JamKazamers, you can visit our Community Forum. +

+ +

+
+
+ Again, welcome to JamKazam, and we hope you have a great time here! +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file 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 new file mode 100644 index 000000000..52dc8ab65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb @@ -0,0 +1,43 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. + + +Getting Started +--------------- + +There are basically three kinds of setups you can use to play on JamKazam. + +* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way. + +* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here. + +* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product. + + +JamKazam Features +----------------- + +JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam: + +* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk + +* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU + +* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA + +* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc + +* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY + + +Getting Help +------------ + +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/. + +Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! + +Best Regards, +Team JamKazam + 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 new file mode 100644 index 000000000..af2681325 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb @@ -0,0 +1,41 @@ +<% provide(:title, @subject) %> + +

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

+
+ +<% @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. + <% 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. + <% 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. + <% 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_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb new file mode 100644 index 000000000..2e18fa163 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb @@ -0,0 +1,31 @@ +<% provide(:title, @subject) %> + +You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details: + +<% @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 %> +<% 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 %> +<% 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 %> +<% 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 new file mode 100644 index 000000000..bdd291b49 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb @@ -0,0 +1,15 @@ +<% provide(:title, @subject) %> + +

+ <% 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 %> + When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% 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 %> +

+
+ +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.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb new file mode 100644 index 000000000..8cc72bce5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> + + <% 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 %> +When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% 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 %> + +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 new file mode 100644 index 000000000..7e76b8c0c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + You have confirmed a lesson request. + <% 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 +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb new file mode 100644 index 000000000..6823a39f7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +You have confirmed a lesson request for <%= @sender.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..c0095a170 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @student.name %> have been canceled. + + <% else %> + Your lesson with <%= @student.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

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

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb new file mode 100644 index 000000000..4a60fe695 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

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

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb new file mode 100644 index 000000000..e895471a3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "You successfully completed a lesson with #{@student.name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> will first be billed and you should receive your payment in the next 48 hours. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb new file mode 100644 index 000000000..30e884e71 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb @@ -0,0 +1,3 @@ +You successfully completed a lesson with <%= @student.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb new file mode 100644 index 000000000..765f13b99 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@student.name} has proposed a different time for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> has proposed a different time for their lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb new file mode 100644 index 000000000..050917a31 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @student.name %> has proposed a different time for their lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb new file mode 100644 index 000000000..5b8bc3891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. +

+ +

+ 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..0347dc3be --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb @@ -0,0 +1,9 @@ +Hello <%= @teacher.name %>, + +Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. + +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. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb new file mode 100644 index 000000000..8ec5b345c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your JamClass lesson today with #{@student.first_name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ Your student <%= @student.name %> will be billed for today's lesson, and you should receive payment within 48 hours. +

+ +

+ 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. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb new file mode 100644 index 000000000..08db0f67e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb @@ -0,0 +1,11 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will be billed for today's lesson, and you will receive payment within 24 hours. + +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. + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb new file mode 100644 index 000000000..7b24c238e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your student #{@student.name} will not be charged for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

Your student <%= @student.name %> will not be billed for today's session. +
+
+ 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/teacher_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..9b9f3b91c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will not be billed for today's session. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb new file mode 100644 index 000000000..a82e118cb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson Request from #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

This student has requested to schedule a <%= @lesson_booking.display_type %> lesson.

Click the button below to get more information and to respond to this lesson request. You must respond to this lesson request promptly, or it will be cancelled, thank you!

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb new file mode 100644 index 000000000..c28cf0142 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb @@ -0,0 +1,3 @@ +<%= @sender.name %> has requested a lesson. + +To see this lesson request, click here: <%= @lesson_booking.home_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb new file mode 100644 index 000000000..ff9c4a3b6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.student.name %> have been rescheduled. + + <% if @message.present? %> +

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

Click the button below to get more information and to update 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 +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb new file mode 100644 index 000000000..5fc1c92a5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.student.name %> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file 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 new file mode 100644 index 000000000..67590256e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb new file mode 100644 index 000000000..fbee04816 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb @@ -0,0 +1,21 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

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

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. + <% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. + <% end %> +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb new file mode 100644 index 000000000..8807cbbda --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. +<% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. +<% end %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb new file mode 100644 index 000000000..177f38c92 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb @@ -0,0 +1,119 @@ +<% provide(:title, 'Welcome to JamKazam!') %> + + +<% if !@user.anonymous? %> +

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

+<% end %> + + +

We're delighted you have decided to join the JamKazam community of musicians, and we hope + + you will enjoy using JamKazam and JamTracks to play more music. Following are some + + resources and some things you can do to get the most out of JamKazam. +

+ + +

Playing With JamTracks
+ + JamTracks are the best way to play along with your favorite songs. Far better and different than + + traditional backing tracks, our JamTracks are complete multi-track professional recordings, with + + fully isolated tracks for each part of the music. And our free app and Internet service are packed + + with features that give you unmatched creative freedom to learn, practice, record, play with + + others, and share your performances. Here are some great JamTracks resources: +

+ + +

Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over + + the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio + + quality. You can set up online sessions that are public or private, for you alone or for others to + + join. You can find and join others’ sessions, use backing tracks and loops in sessions, make + + audio and video recordings of session performances, and more. Click here for a set of tutorial + + videos that show how to use these features. +

+ +

Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how + + unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and + + student playing together at all. JamKazam is a terrific service for teaching and taking online + + music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and + + your students in getting set up and ready to go. +

+ +

Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who + + you are as a musician, as well as your musical interests. For example, what instruments do you + + play? What musical genres do you like best? Are you interested in getting into a virtual/online + + or a real-world band? And so on. Filling out your profile will help you connect with others with + + common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then + + click on the Profile tile, and click the Edit Profile button. +

+ +

Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do + + this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the + + user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in + + this menu, and you can then use the options to invite your friends using their email addresses, + + or via Facebook, or using your Google contacts. +

+ +

Get Help
+ + If you run into trouble and need help, please reach out to us. We will be glad to do everything + + we can to answer your questions and get you up and running. You can visit our Support Portal + + to find knowledge base articles and post questions that have not already been answered. You + + can email us at support@jamkazam.com. And if you just want to chat, share tips and war + + stories, and hang out with fellow JamKazamers, you can visit our Community Forum. +

+ +

+
+
+ Again, welcome to JamKazam, and we hope you have a great time here! +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb new file mode 100644 index 000000000..52dc8ab65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb @@ -0,0 +1,43 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. + + +Getting Started +--------------- + +There are basically three kinds of setups you can use to play on JamKazam. + +* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way. + +* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here. + +* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product. + + +JamKazam Features +----------------- + +JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam: + +* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk + +* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU + +* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA + +* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc + +* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY + + +Getting Help +------------ + +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/. + +Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb index 5b7ce4aec..c80b0490c 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb @@ -5,13 +5,14 @@ JamKazam @@ -25,15 +26,22 @@ - + +

<%= yield(:title) %>

+

+ <%= yield(:title) %>

+

<%= yield %>


- +

+

+ +

<%= yield(:note) %>

-
+
@@ -43,24 +51,30 @@ <% unless @suppress_user_has_account_footer == true %> - - - - - + + +
+
+ + +
- -

-

This email was sent to you because you have an account at JamKazam. -

+ +

+

+ This email was sent to you because + you have an account at JamKazam. +

- - + + <% end %> -
Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved. + + Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb new file mode 100644 index 000000000..67ce3ee7f --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb @@ -0,0 +1,72 @@ + + + + + JamKazam + + + + + + + + + + +
JamKazam
+ + + + + + + + + + <% unless @suppress_user_has_account_footer == true %> + + + + <% end %> +

+ <%= yield(:title) %>

+ + + <%= yield %> + + +
+ + + + +
+ + +

+

+ This email was sent to you because + you have an account at JamKazam. +

+ +
+ + + + +
+ Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved. +
+ + + diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb new file mode 100644 index 000000000..dbc71434a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb @@ -0,0 +1,7 @@ +<%= yield %> + +<% unless @user.nil? || @suppress_user_has_account_footer == true %> +This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. To unsubscribe: https://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>. +<% end %> + +Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index 60dfe9f1b..465d59e37 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -54,4 +54,7 @@ module NotificationTypes MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + # jamclass + LESSON_MESSAGE = "LESSON_MESSAGE" + SCHEDULED_JAMCLASS_INVITATION = "SCHEDULED_JAMCLASS_INVITATION" end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/stats.rb b/ruby/lib/jam_ruby/lib/stats.rb index 697965526..17506a27e 100644 --- a/ruby/lib/jam_ruby/lib/stats.rb +++ b/ruby/lib/jam_ruby/lib/stats.rb @@ -82,8 +82,32 @@ module JamRuby return if self.ignore # doing any writes in a test environment cause annoying puts to occur if @client && data && data.length > 0 - data['host'] = @host - data['time'] = Time.now.to_i + if data.has_key?('values') || data.has_key?(:values) + @client.write_point(name, data) + data['timestamp'] = Time.now.to_i + + tags = data['tags'] + key = 'tags' if tags + tags ||= data[:tags] + key = :tags if key.nil? + tags ||= {} + key = :tags if key.nil? + + tags['host'] = @host + data[key] = tags + else + tags = {} + values = {} + for k,v in data + if v.is_a?(String) + tags[k] = v + else + values[k] = v + end + end + data = {tags: tags, values: values} + end + @client.write_point(name, data) end end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 0cb05e60e..30763c028 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -524,21 +524,41 @@ module JamRuby ) end - def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) - scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + def scheduled_jamclass_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at, lesson_session_id) + scheduled_jamclas_invitation = Jampb::ScheduledJamclassInvitation.new( :session_id => session_id, :photo_url => photo_url, :msg => msg, :session_name => session_name, :session_date => session_date, :notification_id => notification_id, - :created_at => created_at + :created_at => created_at, + lesson_session_id: lesson_session_id ) Jampb::ClientMessage.new( - :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :type => ClientMessage::Type::SCHEDULED_JAMCLASS_INVITATION, :route_to => USER_TARGET_PREFIX + receiver_id, - :scheduled_session_invitation => scheduled_session_invitation + :scheduled_jamclass_invitation => scheduled_jamclas_invitation + ) + end + + + def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) + scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :session_name => session_name, + :session_date => session_date, + :notification_id => notification_id, + :created_at => created_at + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :route_to => USER_TARGET_PREFIX + receiver_id, + :scheduled_session_invitation => scheduled_session_invitation ) end @@ -949,6 +969,29 @@ module JamRuby ) end + # creates the general purpose text message + def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose, lesson_session_id) + lesson_message = Jampb::LessonMessage.new( + :photo_url => sender_photo_url, + :sender_name => sender_name, + :sender_id => sender_id, + :receiver_id => receiver_id, + :msg => msg, + :notification_id => notification_id, + :music_session_id => music_session_id, + :created_at => created_at, + :student_directed => student_directed, + :purpose => purpose, + :lesson_session_id => lesson_session_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LESSON_MESSAGE, + :route_to => USER_TARGET_PREFIX + receiver_id, + :lesson_message => lesson_message + ) + end + # creates the chat message def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel) chat_message = Jampb::ChatMessage.new( diff --git a/ruby/lib/jam_ruby/models/affiliate_distribution.rb b/ruby/lib/jam_ruby/models/affiliate_distribution.rb new file mode 100644 index 000000000..68988748e --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_distribution.rb @@ -0,0 +1,19 @@ +module JamRuby + class AffiliateDistribution < ActiveRecord::Base + + + belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem' + belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + validates :affiliate_referral, presence:true + validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false} + + def self.create(affiliate_referral, fee_in_cents, sale_line_item) + distribution = AffiliateDistribution.new + distribution.affiliate_referral = affiliate_referral + distribution.affiliate_referral_fee_in_cents = fee_in_cents + distribution.sale_line_item = sale_line_item + distribution + end + end +end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index e88aed9c7..aaf05fc35 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -9,6 +9,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :affiliate_distributions, :class_name => "JamRuby::AffiliateDistribution", foreign_key: :affiliate_referral_id attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } @@ -118,17 +119,19 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base sale_time - user.created_at < 2.years end - def should_attribute_sale?(shopping_cart) + def should_attribute_sale?(shopping_cart, user_to_check, instance) - if created_within_affiliate_window(shopping_cart.user, Time.now) - product_info = shopping_cart.product_info + if created_within_affiliate_window(user_to_check, Time.now) + product_info = shopping_cart.product_info(instance) # subtract the total quantity from the freebie quantity, to see how much we should attribute to them real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i - {fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round} + + applicable_rate = shopping_cart.is_lesson? ? lesson_rate : rate + + {fee_in_cents: (product_info[:price] * 100 * real_quantity * applicable_rate.to_f).round} else false end - end def cumulative_earnings_in_dollars @@ -233,23 +236,23 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base def self.sale_items_subquery(start_date, end_date, table_name) %{ - FROM sale_line_items + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.created_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.created_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id } end def self.sale_items_refunded_subquery(start_date, end_date, table_name) %{ - FROM sale_line_items + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.affiliate_refunded_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id AND - sale_line_items.affiliate_refunded = TRUE + affiliate_distributions.affiliate_refunded = TRUE } end # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id @@ -269,22 +272,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base last_updated = NOW(), jamtracks_sold = COALESCE( - (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) + COALESCE( - (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0), due_amount_in_cents = COALESCE( - (SELECT SUM(affiliate_referral_fee_in_cents) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) @@ -322,22 +325,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base last_updated = NOW(), jamtracks_sold = COALESCE( - (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) + COALESCE( - (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0), due_amount_in_cents = COALESCE( - (SELECT SUM(affiliate_referral_fee_in_cents) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) diff --git a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb new file mode 100644 index 000000000..7955fc3a1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb @@ -0,0 +1,51 @@ +module JamRuby + class AffiliatePaymentCharge < Charge + + has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.teacher + end + + def charged_user + teacher + end + + def do_charge + + # source will let you supply a token. But... how to get a token in this case? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + UserMailer.teacher_distribution_done(teacher_payment) + end + + def do_send_unable_charge + UserMailer.teacher_distribution_fail(teacher_payment) + end + + def construct_description + teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb new file mode 100644 index 000000000..5af2b3b5a --- /dev/null +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -0,0 +1,134 @@ +module JamRuby + class Charge < ActiveRecord::Base + + validates :sent_billing_notices, inclusion: {in: [true, false]} + + def max_retries + raise "not implemented" + end + def do_charge(force) + raise "not implemented" + end + def do_send_notices + raise "not implemented" + end + def do_send_unable_charge + raise "not implemented" + end + def charge_retry_hours + 24 + end + def charged_user + raise "not implemented" + end + + def charge(force = false) + + stripe_charge = nil + + if !self.billed + + # check if we can bill at the moment + if !force && last_billing_attempt_at && (charge_retry_hours.hours.ago < last_billing_attempt_at) + return false + end + + if !force && !billing_should_retry + return false + end + + + # bill the user right now. if it fails, move on; will be tried again + self.billing_attempts = self.billing_attempts + 1 + self.billing_should_retry = self.billing_attempts < max_retries + self.last_billing_attempt_at = Time.now + self.save(validate: false) + + begin + + stripe_charge = do_charge(force) + self.stripe_charge_id = stripe_charge.id + self.billed = true + self.billed_at = Time.now + self.save(validate: false) + rescue Stripe::StripeError => e + + stripe_handler(e) + + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (stripe)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + do_send_unable_charge + + return false + rescue Exception => e + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (unhandled)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + unhandled_handler(e) + return false + end + + end + + if !self.sent_billing_notices + # If the charge is successful, then we post the charge to the student’s payment history, + # and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email + + do_send_notices + + self.sent_billing_notices = true + self.sent_billing_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(validate: false) + end + + return stripe_charge + end + + def unhandled_handler(e, reason = 'unhandled_exception') + self.billing_error_reason = reason + if e.cause + self.billing_error_detail = e.cause.to_s + "\n" + e.cause.backtrace.join("\n\t") if e.cause.backtrace + self.billing_error_detail << "\n\n" + self.billing_error_detail << e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + else + self.billing_error_detail = e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + end + self.save(validate: false) + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + def stripe_handler(e) + + msg = e.to_s + + if msg.include?('declined') + self.billing_error_reason = 'card_declined' + self.billing_error_detail = msg + elsif msg.include?('expired') + self.billing_error_reason = 'card_expired' + self.billing_error_detail = msg + elsif msg.include?('processing') + self.billing_error_reason = 'processing_error' + self.billing_error_detail = msg + else + self.billing_error_reason = 'stripe' + self.billing_error_detail = msg + end + + self.save(validate: false) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 8e650bad7..3c6b056db 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -3,18 +3,42 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:message] + CHANNEL_LESSON = 'lesson' self.table_name = 'chat_messages' self.primary_key = 'id' default_scope order('created_at DESC') + attr_accessor :ignore_message_checks + attr_accessible :user_id, :message, :music_session_id belongs_to :user belongs_to :music_session + belongs_to :target_user, class_name: "JamRuby::User" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" validates :user, presence: true - validates :message, length: {minimum: 1, maximum: 255}, no_profanity: 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) + 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 + + if lesson_booking + chat_msg.ignore_message_checks = true + end + + if chat_msg.save + ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel + end + chat_msg + end class << self @@ -36,7 +60,7 @@ module JamRuby query = ChatMessage.where('music_session_id = ?', music_session_id) end - query = query.offset(start).limit(limit).order('created_at DESC') + query = query.offset(start).limit(limit).order('created_at DESC').includes([:user]) if query.length == 0 [query, nil] diff --git a/ruby/lib/jam_ruby/models/invitation.rb b/ruby/lib/jam_ruby/models/invitation.rb index b31a816f6..45c24505f 100644 --- a/ruby/lib/jam_ruby/models/invitation.rb +++ b/ruby/lib/jam_ruby/models/invitation.rb @@ -1,6 +1,7 @@ module JamRuby class Invitation < ActiveRecord::Base + INVITATION_NOT_TEACHER_VALIDATION_ERROR = "Lessons can only sent invitations to teachers" FRIENDSHIP_REQUIRED_VALIDATION_ERROR = "You can only invite friends" MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION = "You must be a member of the music session to send invitations on behalf of it" JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION = "You can only associate a join request with an invitation if that join request comes from the invited user and if it's for the same music session" @@ -15,7 +16,7 @@ module JamRuby validates :receiver, :presence => true validates :music_session, :presence => true - validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join + validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join_or_teacher private @@ -25,15 +26,22 @@ module JamRuby end end - def require_are_friends_or_requested_to_join + def require_are_friends_or_requested_to_join_or_teacher if !join_request.nil? && (join_request.user != receiver || join_request.music_session != music_session) errors.add(:join_request, JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION ) elsif join_request.nil? # we only check for friendship requirement if this was not in response to a join_request - unless receiver.friends.exists? sender - errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + if !receiver.friends.exists?(sender) && (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + if !receiver.friends.exists?(sender) + errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + elsif (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + errors.add(:receiver, INVITATION_NOT_TEACHER_VALIDATION_ERROR) + end + + end end + end end end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 1ad7fb09d..72febb08c 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -338,6 +338,7 @@ module JamRuby query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') + query = query.includes([{ jam_track_tracks: :instrument }, { genres_jam_tracks: :genre }]) else query = query.group("jam_tracks.id") if options[:sort_by] == 'jamtrack' diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb new file mode 100644 index 000000000..384009aaa --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -0,0 +1,826 @@ +# represenst the type of lesson package +module JamRuby + class LessonBooking < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:description, :cancel_message] + + include ActiveModel::Dirty + + @@log = Logging.logger[LessonBooking] + + attr_accessor :accepting, :countering, :countered_slot, :countered_lesson + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED] + + LESSON_TYPE_FREE = 'single-free' + LESSON_TYPE_TEST_DRIVE = 'test-drive' + LESSON_TYPE_PAID = 'paid' + + LESSON_TYPES = [LESSON_TYPE_FREE, LESSON_TYPE_TEST_DRIVE, LESSON_TYPE_PAID] + + PAYMENT_STYLE_ELSEWHERE = 'elsewhere' + PAYMENT_STYLE_SINGLE = 'single' + PAYMENT_STYLE_WEEKLY = 'weekly' + PAYMENT_STYLE_MONTHLY = 'monthly' + + + PAYMENT_STYLES = [PAYMENT_STYLE_ELSEWHERE, PAYMENT_STYLE_SINGLE, PAYMENT_STYLE_WEEKLY, PAYMENT_STYLE_MONTHLY] + + belongs_to :user, class_name: "JamRuby::User" + 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" + + validates :user, presence: true + validates :teacher, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :recurring, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :card_presumed_ok, inclusion: {in: [true, false]} + validates :active, inclusion: {in: [true, false]} + validates :lesson_length, inclusion: {in: [30, 45, 60, 90, 120]} + validates :payment_style, inclusion: {in: PAYMENT_STYLES} + validates :booked_price, presence: true + validates :description, no_profanity: true, length: {minimum: 10, maximum: 20000} + + validate :validate_user, on: :create + validate :validate_recurring + validate :validate_lesson_booking_slots + validate :validate_lesson_length + validate :validate_payment_style + validate :validate_accepted, :if => :accepting + + + before_save :before_save + before_validation :before_validation + after_create :after_create + around_save :around_update + + scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) } + scope :active, -> { where(active: true) } + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :engaged, -> { where("status = '#{STATUS_APPROVED}' OR status = '#{STATUS_REQUESTED}' OR status = '#{STATUS_SUSPENDED}'") } + + def before_validation + if self.booked_price.nil? + self.booked_price = compute_price + end + end + + def after_create + if card_presumed_ok && !sent_notices + send_notices + end + end + + def before_save + automatically_default_slot + end + + def around_update + + @default_slot_did_change = self.default_slot_id_changed? + + yield + + sync_lessons + sync_remaining_test_drives + + @default_slot_did_change = nil + @accepting = nil + @countering = nil + end + + # here for shopping_cart + def product_info + {price: booked_price, real_price: booked_price, total_price: booked_price} + end + # here for shopping_cart + def price + booked_price + end + + + def alt_slot + found = nil + lesson_booking_slots.each do |slot| + if slot.id != default_slot.id + found = slot + break + end + end + found + end + + def student + user + end + + 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 + LessonSession.find(session.id) if session + else + lesson_sessions[0] + end + end + + def accept(lesson_session, slot, accepter) + if !is_active? + self.accepting = true + end + + self.active = true + self.status = STATUS_APPROVED + self.counter_slot = nil + self.default_slot = slot + self.accepter = accepter + + success = self.save + + if !success + puts "unable to accept lesson booking #{errors.inspect}" + end + success + end + + def counter(lesson_session, proposer, slot) + self.countering = true + self.lesson_booking_slots << slot + self.counter_slot = slot + #self.status = STATUS_COUNTERED + self.save + end + + def automatically_default_slot + if is_requested? + if lesson_booking_slots.length > 0 + self.default_slot = lesson_booking_slots[0] + end + end + end + + def sync_remaining_test_drives + if is_test_drive? || is_single_free? + if card_presumed_ok && !user_decremented + self.user_decremented = true + self.save(validate: false) + if is_single_free? + user.remaining_free_lessons = user.remaining_free_lessons - 1 + elsif is_test_drive? + user.remaining_test_drives = user.remaining_test_drives - 1 + end + user.save(validate: false) + end + end + end + + def create_minimum_booking_time + # trying to be too smart + #(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + Time.now + end + + def sync_lessons + + if is_canceled? + # don't create new sessions if cancelled + return + end + + + if @default_slot_did_change + + end + # Here we go; let's create a lesson(s) as needed + + # we need to make lessons into the future a bit, to give time for everyone involved + minimum_start_time = create_minimum_booking_time + + # get all sessions that are already scheduled for this booking ahead of the minimum time + sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).order(:created_at) + + if @default_slot_did_change + # # adjust all session times + + offset = 0 + sessions.each_with_index do |item, i| + item.lesson_session.slot = default_slot + result = item.lesson_session.update_next_available_time(offset) + if result + offset = result + offset += 1 + end + end + end + + needed_sessions = determine_needed_sessions(sessions) + + # if the latest scheduled session is after the minimum start time, then bump up minimum start time + last_session = sessions.last + last_session.reload if last_session # because of @default_slot_did_change logic above, this can be necessary + + if last_session && last_session.scheduled_start && last_session.scheduled_start > minimum_start_time + minimum_start_time = last_session.scheduled_start + end + times = default_slot.scheduled_times(needed_sessions, minimum_start_time) + + scheduled_lessons(times) + end + + # sensitive to current time + def predicted_times_for_month(year, month) + first_day = Date.new(year, month, 1) + last_day = Date.new(year, month, -1) + sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start >= ?", first_day).where("scheduled_start <= ?", last_day).order(:created_at) + + times = [] + + sessions.each do |session| + times << session.scheduled_start + end + last_session = sessions.last + + start_day = first_day + if last_session + start_day = last_session.scheduled_start.to_date + 1 + end + + # now flesh out the rest of the month with predicted times + + more_times = default_slot.scheduled_times(5, start_day) + + more_times.each do |time| + if time.to_date >= first_day && time.to_date <= last_day + times << time + end + end + times + end + + def determine_needed_sessions(sessions) + needed_sessions = 0 + if is_requested? + # in the case of a requested booking (not approved) only make one, even if it's recurring. This is for UI considerations + if sessions.count == 0 + needed_sessions = 1 + end + elsif is_active? + expected_num_sessions = recurring ? 2 : 1 + needed_sessions = expected_num_sessions - sessions.count + end + needed_sessions + end + + def scheduled_lessons(times) + times.each do |time| + + lesson_session = LessonSession.create(self) + + if lesson_session.errors.any? + puts "JamClass lesson session creation errors #{lesson_session.errors.inspect}" + @@log.error("JamClass lesson session creation errors #{lesson_session.errors.inspect}") + raise ActiveRecord::Rollback + end + ms_tz = ActiveSupport::TimeZone.new(default_slot.timezone) + ms_tz = "#{ms_tz.name},#{default_slot.timezone}" + + rsvps = [{instrument_id: 'other', proficiency_level: 0, approve: true}] + + music_session = MusicSession.create(student, { + name: "#{display_type2} JamClass taught by #{teacher.name}", + description: "This is a #{lesson_length}-minute #{display_type2} lesson with #{teacher.name}.", + musician_access: false, + fan_access: false, + genres: ['other'], + approval_required: false, + fan_chat: false, + legal_policy: "standard", + language: 'eng', + duration: lesson_length, + recurring_mode: false, + timezone: ms_tz, + create_type: MusicSession::CREATE_TYPE_LESSON, + is_unstructured_rsvp: true, + scheduled_start: time, + invitations: [teacher.id], + lesson_session: lesson_session, + rsvp_slots: rsvps + }) + + if music_session.errors.any? + puts "JamClass lesson scheduling errors #{music_session.errors.inspect}" + @@log.error("JamClass lesson scheduling errors #{music_session.errors.inspect}") + raise ActiveRecord::Rollback + end + + if lesson_session.is_active? + # send out email to student to act as something they can add to their calendar + Notification.send_student_jamclass_invitation(music_session, student) + end + + end + end + + def is_weekly_payment? + payment_style == PAYMENT_STYLE_WEEKLY + end + + def is_monthly_payment? + payment_style == PAYMENT_STYLE_MONTHLY + end + + def requires_per_session_billing? + is_normal? && !is_monthly_payment? + end + + def requires_teacher_distribution?(target) + if target.is_a?(JamRuby::LessonSession) + is_test_drive? || (is_normal? && !is_monthly_payment?) + elsif target.is_a?(JamRuby::LessonPackagePurchase) + is_monthly_payment? + else + raise "unable to determine object type of #{target}" + end + end + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_active? + active + end + + def validate_accepted + # accept is multipe purpose; either accept the initial request, or a counter slot + if self.status_was != STATUS_REQUESTED && counter_slot.nil? # && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This lesson is already #{self.status}.") + end + + self.accepting = false + end + + def send_notices + UserMailer.student_lesson_request(self).deliver + UserMailer.teacher_lesson_request(self).deliver + Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept' + self.sent_notices = true + self.save + end + + def lesson_package_type + if is_single_free? + LessonPackageType.single_free + elsif is_test_drive? + LessonPackageType.test_drive + elsif is_normal? + LessonPackageType.single + end + end + + def display_type2 + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + "Single" + end + end + + def display_type + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + if recurring + "recurring" + else + "single" + end + + end + end + + # determine the price of this booking based on what the user wants, and the teacher's pricing + + def compute_price + if is_single_free? + 0 + elsif is_test_drive? + LessonPackageType.test_drive.price + elsif is_normal? + teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY) + end + end + + def distribution_price_in_cents + if is_single_free? + 0 + elsif is_test_drive? + 10 * 100 + elsif is_normal? + booked_price * 100 + end + end + + def is_single_free? + lesson_type == LESSON_TYPE_FREE + end + + def is_test_drive? + lesson_type == LESSON_TYPE_TEST_DRIVE + end + + def is_normal? + lesson_type == LESSON_TYPE_PAID + end + + def dayWeekDesc(slot = default_slot) + day = case slot.day_of_week + when 0 then "Sunday" + when 1 then "Monday" + when 2 then "Tuesday" + when 3 then "Wednesday" + when 4 then "Thursday" + when 5 then "Friday" + when 6 then "Saturday" + end + + + if slot.hour > 11 + hour = slot.hour - 12 + if hour == 0 + hour = 12 + end + am_pm = 'pm' + else + hour = slot.hour + if hour == 0 + hour = 12 + end + am_pm = 'am' + end + + "#{day} at #{hour}:#{slot.minute}#{am_pm}" + + end + + def approved_before? + !self.accepter_id.nil? + end + def cancel(canceler, other, message) + + self.active = false + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + success = save + if success + 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" + 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" + 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" + 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) + + end + + success + end + + def card_approved + self.card_presumed_ok = true + if self.save && !sent_notices + send_notices + end + end + + def validate_user + if card_presumed_ok && is_single_free? + if !user.has_free_lessons? + errors.add(:user, 'have no remaining free lessons') + end + + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + elsif is_test_drive? + if user.has_requested_test_drive?(teacher) && !user.admin + errors.add(:user, "has 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") + end + elsif is_normal? + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + end + end + + def validate_teacher + # shouldn't we check if the teacher already has a booking in this time slot, or at least warn the user + end + + def validate_recurring + if is_single_free? || is_test_drive? + if recurring + errors.add(:recurring, "can not be true for this type of lesson") + end + end + + false + end + + def validate_lesson_booking_slots + if lesson_booking_slots.length == 0 || lesson_booking_slots.length == 1 + errors.add(:lesson_booking_slots, "must have two times specified") + end + end + + def validate_lesson_length + if is_single_free? || is_test_drive? + if lesson_length != 30 + errors.add(:lesson_length, "must be 30 minutes") + end + end + end + + def validate_payment_style + if is_normal? + if payment_style.nil? + errors.add(:payment_style, "can't be blank") + end + end + 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) + end + + def self.book_test_drive(user, teacher, lesson_booking_slots, description) + self.book(user, teacher, LessonBooking::LESSON_TYPE_TEST_DRIVE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) + end + + def self.book_normal(user, teacher, lesson_booking_slots, description, recurring, payment_style, lesson_length) + self.book(user, teacher, LessonBooking::LESSON_TYPE_PAID, lesson_booking_slots, recurring, lesson_length, payment_style, description) + end + + def self.book(user, teacher, lesson_type, lesson_booking_slots, recurring, lesson_length, payment_style, description) + + lesson_booking = nil + LessonBooking.transaction do + + lesson_booking = LessonBooking.new + lesson_booking.user = user + lesson_booking.card_presumed_ok = user.has_stored_credit_card? + lesson_booking.sent_notices = false + lesson_booking.teacher = teacher + lesson_booking.lesson_type = lesson_type + lesson_booking.recurring = recurring + lesson_booking.lesson_length = lesson_length + lesson_booking.payment_style = payment_style + lesson_booking.description = description + lesson_booking.status = STATUS_REQUESTED + + # two-way association slots, for before_validation loic in slot to work + lesson_booking.lesson_booking_slots = lesson_booking_slots + lesson_booking_slots.each do |slot| + slot.lesson_booking = lesson_booking + slot.message = description + 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) + end + end + lesson_booking + end + + def self.unprocessed(current_user) + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false) + end + + def self.requested(current_user) + LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED) + 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') + end + + # check for any recurring sessions where there are not at least 2 sessions into the future. If not, we need to make sure they get made + def self.hourly_check + schedule_upcoming_lessons + bill_monthlies + end + + def self.bill_monthlies + now = Time.now + billable_monthlies(now).each do |lesson_booking| + lesson_booking.bill_monthly(now) + end + + today = now.to_date + seven_days_in_future = today + 7 + + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + next_month = seven_days_in_future.to_time + billable_monthlies(next_month).each do |lesson_booking| + lesson_booking.bill_monthly(next_month) + end + end + end + + def self.billable_monthlies(now) + current_month_first_day = Date.new(now.year, now.month, 1) + current_month_last_day = Date.new(now.year, now.month, -1) + #next_month_last_day = now.month == 12 ? Date.new(now.year + 1, 1, -1) : Date.new(now.year, now.month + 1, -1) + + LessonBooking + .joins(:lesson_sessions => :music_session) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND (lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}))") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .active + .where('music_sessions.scheduled_start >= ?', current_month_first_day) + .where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq + +=begin + today = now.to_date + seven_days_in_future = today + 7 + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + condition = "(((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) ) + OR ((lesson_package_purchases.year = #{seven_days_in_future.year} AND lesson_package_purchases.month = #{seven_days_in_future.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{seven_days_in_future.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{seven_days_in_future.month} ) ) )" + else + condition = "((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) )" + end + + + # .where("(lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) OR (lesson_package_purchases.year = #{next_month_last_day.year} AND lesson_package_purchases.month = #{next_month_last_day.month})") + + # find any monthly-billed bookings that have a session coming up within 7 days, and if so, attempt to bill them + LessonBooking + .joins(:lesson_sessions) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND #{condition})") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .where(status: STATUS_APPROVED) + .where('lesson_sessions.created_at >= ?', current_month_first_day) + .where('lesson_sessions.created_at <= ?', seven_days_in_future).uniq + +=end + end + + def self.bookings(student, teacher, since_at = nil) + bookings = LessonBooking.where(user_id: student.id, teacher_id: teacher.id) + + if since_at + bookings = bookings.where('created_at >= ?', since_at) + end + + bookings + end + + def self.engaged_bookings(student, teacher, since_at = nil) + bookings = bookings(student, teacher, since_at) + bookings.engaged + end + + def bill_monthly(now) + LessonBooking.transaction do + self.lock! + + current_month = Date.new(now.year, now.month, 1) + + bill_for_month(current_month) + + today = now.to_date + seven_days_in_future = today + 7 + is_different_month = seven_days_in_future.month != today.month + if is_different_month + bill_for_month(seven_days_in_future) + end + end + end + + def bill_for_month(day_in_month) + # try to find lesson package purchase for this month, and last month, and see if they need processing + current_month_purchase = lesson_package_purchases.where(lesson_booking_id: self.id, user_id: student.id, year: day_in_month.year, month: day_in_month.month).first + if current_month_purchase.nil? + current_month_purchase = LessonPackagePurchase.create(user, self, lesson_package_type, day_in_month.year, day_in_month.month) + end + current_month_purchase.bill_monthly + end + + def suspend! + # when this is called, the calling code sends out a email to let the student and teacher know (it feels unnatural it's not here, though) + self.status = STATUS_SUSPENDED + self.active = false + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).suspend! + end + end + end + + def unsuspend! + if self.status == STATUS_SUSPENDED + self.status = STATUS_APPROVED + self.active = true + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).unsuspend! + end + end + end + end + + def future_sessions + lesson_sessions.joins(:music_session).where('scheduled_start > ?', Time.now).order(:created_at) + end + + def self.schedule_upcoming_lessons + minimum_start_time = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + + lesson_bookings = find_bookings_needing_sessions(minimum_start_time) + + lesson_bookings.each do |data| + lesson_booking = LessonBooking.find(data["lesson_booking_id"]) + lesson_booking.sync_lessons + end + end + + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_bookings/" + id + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb new file mode 100644 index 000000000..758bbc319 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -0,0 +1,239 @@ +# represenst the type of lesson package +module JamRuby + class LessonBookingSlot < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:message, :accept_message, :cancel_message] + + @@log = Logging.logger[LessonBookingSlot] + + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :proposer, class_name: "JamRuby::User" + has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot + has_one :countered_booking, class_name: "JamRuby::LessonBooking", foreign_key: :counter_slot_id, inverse_of: :counter_slot + has_one :countered_lesson, class_name: "JamRuby::LessonSession", foreign_key: :counter_slot_id, inverse_of: :counter_slot + + SLOT_TYPE_SINGLE = 'single' + SLOT_TYPE_RECURRING = 'recurring' + + SLOT_TYPES = [SLOT_TYPE_SINGLE, SLOT_TYPE_RECURRING] + + validates :proposer, presence: true + validates :slot_type, inclusion: {in: SLOT_TYPES} + #validates :preferred_day + validates :day_of_week, numericality: {only_integer: true}, allow_blank: true # 0 = sunday - 6 = saturday + validates :hour, numericality: {only_integer: true} + validates :minute, numericality: {only_integer: true} + validates :timezone, presence: true # example: 'America/New_York' + validates :update_all, inclusion: {in: [true, false]} + + validate :validate_slot_type + validate :validate_slot_minimum_time, on: :create + validate :validate_proposer + before_validation :before_validation + + def before_validation + if proposer.nil? + self.proposer = container.student + end + end + + def container + if lesson_booking + lesson_booking + else + lesson_session + end + end + + def is_teacher_created? + self.proposer == container.teacher + end + + def is_student_created? + !is_teacher_created? + end + + def is_teacher_approved? + !is_teacher_created? + end + + def recipient + if is_teacher_created? + container.student + else + container.teacher + end + end + + def create_minimum_booking_time + (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60 * 60) + end + + def scheduled_times(needed_sessions, minimum_start_time) + + times = [] + week_offset = 0 + + needed_sessions.times do |i| + candidate = scheduled_time(i + week_offset) + + if day_of_week && candidate <= minimum_start_time + # move it up a week + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + # sanity check + if candidate <= minimum_start_time + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + if candidate <= minimum_start_time + raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}" + end + end + end + times << candidate + end + + times + end + + def next_day + date = Date.today + date += ((day_of_week - date.wday) % 7).abs + end + + # weeks is the number of weeks in the future to compute the time for + def scheduled_time(weeks) + + # get the timezone of the slot, so we can compute times + tz = TZInfo::Timezone.get(timezone) + + if preferred_day + time = tz.local_to_utc(Time.new(preferred_day.year, preferred_day.month, preferred_day.day, hour, minute, 0)) + else + adjusted = next_day + (weeks * 7) + # day of the week adjustment + time = tz.local_to_utc(Time.new(adjusted.year, adjusted.month, adjusted.day, hour, minute, 0)) + end + + time + end + + def lesson_length + safe_lesson_booking.lesson_length + end + + def safe_lesson_booking + found = lesson_booking + found ||= lesson_session.lesson_booking + end + + def pretty_scheduled_start(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + + duration = lesson_length * 60 # convert from minutes to seconds + 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} #{tz}" + else + "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" + end + end + + def pretty_start_time(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip} #{tz.name}" + else + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip}" + end + end + + def validate_proposer + if proposer && (proposer != container.student && proposer != container.teacher) + errors.add(:proposer, "must be either the student or teacher") + end + end + + def validate_slot_type + if slot_type == SLOT_TYPE_SINGLE + if preferred_day.nil? + errors.add(:preferred_day, "must be specified") + end + end + + if slot_type == SLOT_TYPE_RECURRING + if day_of_week.nil? + errors.add(:day_of_week, "must be specified") + end + end + end + + def validate_slot_minimum_time + + # this code will fail miserably if the slot is malformed + if errors.any? + return + end + + if is_teacher_created? + return # the thinking is that a teacher can propose much tighter to the time; since they only counter; maybe they talked to the student + end + + # + # minimum_start_time = create_minimum_booking_time + minimum_start_time = Time.now + + if day_of_week + # this is recurring; it will sort itself out + else + time = scheduled_time(0) + + if time <= minimum_start_time + #errors.add(:base, "must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours in the future") + errors.add(:preferred_day, "can not be in the past") + end + end + + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb new file mode 100644 index 000000000..7d0e98e4d --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -0,0 +1,147 @@ +# represents the purchase of a LessonPackage +module JamRuby + class LessonPackagePurchase < ActiveRecord::Base + + @@log = Logging.logger[LessonPackagePurchase] + + 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 + + # who purchased the lesson package? + belongs_to :user, class_name: "JamRuby::User", :foreign_key => "user_id", inverse_of: :lesson_purchases + belongs_to :lesson_package_type, class_name: "JamRuby::LessonPackageType" + belongs_to :teacher, class_name: "JamRuby::User" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + + has_one :sale_line_item, class_name: "JamRuby::SaleLineItem" + + validates :user, presence: true + validates :lesson_package_type, presence: true + validates :price, presence: true + validate :validate_test_drive, on: :create + + after_create :add_test_drives + after_create :create_charge + + def validate_test_drive + if user + if !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! + end + + def add_test_drives + if self.lesson_package_type.is_test_drive? + new_test_drives = user.remaining_test_drives + 4 + User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) + user.remaining_test_drives = new_test_drives + end + + end + + + def name + lesson_package_type.sale_display + end + + def amount_charged + lesson_payment_charge.amount_in_cents / 100.0 + end + + def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil) + purchase = LessonPackagePurchase.new + purchase.user = user + purchase.lesson_booking = lesson_booking + purchase.teacher = lesson_booking.teacher if lesson_booking + + if year + purchase.year = year + purchase.month = month + purchase.recurring = true + + if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) + purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + 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) + else + purchase.lesson_package_type = lesson_package_type + purchase.price = lesson_package_type.price + end + + purchase.save + purchase + end + + def price_in_cents + (price * 100).to_i + end + + def description(lesson_booking) + lesson_package_type.description(lesson_booking) + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + + def month_name + if recurring + Date.new(year, month, 1).strftime('%B') + else + 'non-monthly paid lesson' + end + end + + def student + user + end + + + def bill_monthly(force = false) + lesson_payment_charge.charge(force) + + if lesson_payment_charge.billed + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + end +end + diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb new file mode 100644 index 000000000..d60aaa14a --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -0,0 +1,106 @@ +# represenst the type of lesson package +module JamRuby + class LessonPackageType < ActiveRecord::Base + + @@log = Logging.logger[LessonPackageType] + + PRODUCT_TYPE = 'LessonPackageType' + + SINGLE_FREE = 'single-free' + TEST_DRIVE = 'test-drive' + SINGLE = 'single' + + LESSON_PACKAGE_TYPES = + [ + SINGLE_FREE, + TEST_DRIVE, + SINGLE + ] + + validates :name, presence: true + validates :description, presence: true + validates :price, presence: true + validates :package_type, presence: true, inclusion: {in: LESSON_PACKAGE_TYPES} + + def self.monthly + LessonPackageType.find(MONTHLY) + end + + def self.single_free + LessonPackageType.find(SINGLE_FREE) + end + + def self.test_drive + LessonPackageType.find(TEST_DRIVE) + end + + def self.single + LessonPackageType.find(SINGLE) + end + + def booked_price(lesson_booking) + if is_single_free? + 0 + elsif is_test_drive? + LessonPackageType.test_drive.price + 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 description(lesson_booking) + if is_single_free? + "Single Free Lesson" + elsif is_test_drive? + "Test Drive" + elsif is_normal? + if lesson_booking.recurring + "Recurring #{lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_WEEKLY ? "Weekly" : "Monthly"} #{lesson_booking.lesson_length}m" + else + "Single #{lesson_booking.lesson_length}m lesson" + end + end + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def is_single_free? + id == SINGLE_FREE + end + + def is_test_drive? + id == TEST_DRIVE + end + + def is_normal? + id == SINGLE + end + + + def sale_display + name + end + + def plan_code + if package_type == SINGLE_FREE + "lesson-package-single-free" + elsif package_type == TEST_DRIVE + "lesson-package-test-drive" + elsif package_type == SINGLE + "lesson-package-single" + else + raise "unknown lesson package type #{package_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb new file mode 100644 index 000000000..f68e84fc6 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -0,0 +1,89 @@ +module JamRuby + class LessonPaymentCharge < Charge + + has_one :lesson_session, class_name: "JamRuby::LessonSession", foreign_key: :charge_id + has_one :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase", foreign_key: :charge_id + + def max_retries + 5 + end + + def charged_user + @charged_user ||= target.student + end + + def resolve_target + if is_lesson? + lesson_session + else + lesson_package_purchase + end + end + def target + @target ||= resolve_target + end + + def lesson_booking + @lesson_booking ||= target.lesson_booking + end + + def student + charged_user + end + + def is_lesson? + !lesson_session.nil? + end + + def do_charge(force) + + if is_lesson? + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, lesson_session) + else + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, nil, lesson_package_purchase, force) + lesson_booking.unsuspend! if lesson_booking.is_suspended? + end + + stripe_charge = result[:stripe_charge] + + self.amount_in_cents = stripe_charge.amount + self.save(validate: false) + + # update teacher distribution, because it's now ready to be given to them! + + distribution = target.teacher_distribution + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end + + stripe_charge + end + + def do_send_notices + if is_lesson? + UserMailer.student_lesson_normal_done(lesson_session).deliver + UserMailer.teacher_lesson_normal_done(lesson_session).deliver + else + UserMailer.student_lesson_monthly_charged(lesson_package_purchase).deliver + UserMailer.teacher_lesson_monthly_charged(lesson_package_purchase).deliver + end + end + + def do_send_unable_charge + if is_lesson? + UserMailer.student_unable_charge(lesson_session) + else + if !billing_should_retry + lesson_booking.suspend! + end + + UserMailer.student_unable_charge_monthly(lesson_package_purchase) + if lesson_booking.is_suspended? + # let the teacher know that we are having problems collecting from the student + UserMailer.teacher_unable_charge_monthly(lesson_package_purchase) + end + end + 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 new file mode 100644 index 000000000..dec9512fd --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -0,0 +1,642 @@ +# represenst the type of lesson package +module JamRuby + class LessonSession < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:cancel_message] + + attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling + + + @@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 :pretty_scheduled_start, to: :music_session + + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_MISSED = 'missed' + STATUS_COMPLETED = 'completed' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED] + + LESSON_TYPE_SINGLE = 'paid' + LESSON_TYPE_SINGLE_FREE = 'single-free' + 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" + 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 :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 + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + + + validates :duration, presence: true, numericality: {only_integer: true} + validates :lesson_booking, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :booked_price, presence: true + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :teacher_complete, inclusion: {in: [true, false]} + validates :student_complete, inclusion: {in: [true, false]} + validates :teacher_canceled, inclusion: {in: [true, false]} + validates :student_canceled, inclusion: {in: [true, false]} + validates :success, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :post_processed, inclusion: {in: [true, false]} + + validate :validate_creating, :if => :creating + validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling + + after_save :after_counter, :if => :countering + after_save :manage_slot_changes + after_create :create_charge + + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :completed, -> { where(status: STATUS_COMPLETED) } + scope :missed, -> { where(status: STATUS_MISSED) } + + def create_charge + if !is_test_drive? + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_session = self + lesson_payment_charge.save! + end + end + + def manage_slot_changes + # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . + # TODO: what to do, what to do. + + end + + def suspend! + self.status = STATUS_SUSPENDED + self.save + end + + def unsuspend! + self.status = STATUS_APPROVED + self.save + end + + def self.hourly_check + analyse_sessions + complete_sessions + 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| + 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| + lession_session = music_session.lesson_session + lession_session.session_completed + end + end + + def analyse + if self.analysed + return + end + + analysis = LessonSessionAnalyser.analyse(self) + + self.analysis = analysis_to_json(analysis) + self.success = analysis[:bill] + self.analysed_at = Time.now + self.analysed = true + + if lesson_booking.requires_teacher_distribution?(self) + self.teacher_distribution = TeacherDistribution.create_for_lesson(self) + end + + if self.save + # send out emails appropriate for this type of session + session_completed + end + end + + + def amount_charged + lesson_payment_charge.amount_in_cents / 100.0 + end + + def analysis_to_json(analysis) + json = {} + + analysis.each do |k, v| + if v.is_a?(Array) + array = [] + v.each do |item| + if item.is_a?(Range) + array << {begin: item.begin, end: item.end} + else + raise "expected range" + end + end + json[k] = array + else + json[k] = v + end + end + json.to_json + end + + def session_completed + LessonSession.transaction do + self.lock! + + if post_processed + # nothing to do. because this is an async job, it's possible this was queued up with another session_completed fired + return + end + + if lesson_booking.is_test_drive? + test_drive_completed + elsif lesson_booking.is_normal? + if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment? + recurring_completed + else + normal_lesson_completed + end + end + + end + end + + def bill_lesson + + lesson_payment_charge.charge + + if lesson_payment_charge.billed + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + + 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 + student.test_drive_succeeded(self) + else + student.test_drive_failed(self) + end + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def recurring_completed + if success + if lesson_booking.is_monthly_payment? + # monthly payments are handled at beginning of month; just poke with email, and move on + + if !sent_notices + # not in spec; just poke user and tell them we saw it was successfully completed + UserMailer.monthly_recurring_done(self).deliver + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + else + bill_lesson + end + else + if lesson_booking.is_monthly_payment? + # bad session; just poke user + if !sent_notices + UserMailer.monthly_recurring_no_bill(self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + else + if !sent_notices + # bad session; just poke user + UserMailer.student_weekly_recurring_no_bill(student, self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + end + end + end + + def normal_lesson_completed + if success + bill_lesson + else + if !sent_notices + UserMailer.student_lesson_normal_no_bill(self).deliver + UserMailer.teacher_lesson_no_bill(self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + end + + def after_counter + send_counter(@countered_lesson, @countered_slot) + end + + def scheduled_start + music_session.scheduled_start + end + + def send_counter(countered_lesson, countered_slot) + if countered_slot.is_teacher_created? + UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver + else + UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver + end + self.countering = false + end + + default_scope { order('lesson_sessions.created_at') } + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_completed? + status == STATUS_COMPLETED + end + + def is_missed? + status == STATUS_MISSED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_countered? + status == STATUS_COUNTERED + end + + def validate_creating + if !is_requested? && !is_approved? + self.errors.add(:status, "is not valid for a new lesson session.") + end + + if is_approved? && lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + def validate_accepted + if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This session is already #{self.status_was}.") + end + + if approved_before? + # only checking for this on 1st time through acceptance + if lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + self.accepting = false + end + + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + # check 24 hour window + if scheduled_start.to_i - Time.now.to_i < 24 * 60 * 60 + self.errors.add(:base, "This session is due to start within 24 hours and can not be canceled.") + end + + self.canceling = false + end + + def self.create(booking) + lesson_session = LessonSession.new + lesson_session.creating = true + lesson_session.duration = booking.lesson_length + lesson_session.lesson_type = booking.lesson_type + lesson_session.lesson_booking = booking + lesson_session.booked_price = booking.booked_price + lesson_session.teacher = booking.teacher + lesson_session.status = booking.status + lesson_session.slot = booking.default_slot + if booking.is_test_drive? + lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase + end + lesson_session.save + + if lesson_session.errors.any? + puts "Lesson Session errors #{lesson_session.errors.inspect}" + end + lesson_session + end + + def student + music_session.creator + 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 = query.order('music_sessions.scheduled_start DESC') + + if params[:as_teacher] + query = query.where('lesson_sessions.teacher_id = ?', user.id) + else + query = query.where('music_sessions.user_id = ?', user.id) + end + + + 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 update_scheduled_start(week_offset) + music_session.scheduled_start = slot.scheduled_time(week_offset) + music_session.save! + end + + # grabs the next available time that's after the present, to avoid times being scheduled in the past + def update_next_available_time(attempt = 0) + max_attempts = attempt + 10 + while attempt < max_attempts + test = slot.scheduled_time(attempt) + + if test >= Time.now + time = test + # valid time found! + break + end + attempt += 1 + end + + if time + music_session.scheduled_start = time + music_session.save + end + + time.nil? ? nil : attempt + end + + # teacher accepts the lesson + def accept(params) + response = self + LessonSession.transaction do + + message = params[:message] + slot = params[:slot] + accepter = params[:accepter] + self.slot = slot = LessonBookingSlot.find(slot) + self.slot.accept_message = message + self.slot.save! + self.accepting = true + self.status = STATUS_APPROVED + + if !approved_before? + # 1st time this has ever been approved; there are other things we need to do + + if lesson_package_purchase.nil? && lesson_booking.is_test_drive? + self.lesson_package_purchase = student.most_recent_test_drive_purchase + end + + if self.save + # also let the lesson_booking know we got accepted + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + 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) + Notification.send_jamclass_invitation_teacher(music_session, teacher) + Notification.send_student_jamclass_invitation(music_session, student) + Notification.send_lesson_message('accept', self, true) + + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id}") + puts("unable to accept slot #{slot.id} for lesson #{self.id}") + response = self + raise ActiveRecord::Rollback + end + else + # this implies a new slot has been countered, and now approved + + if self.save + if slot.update_all + if !lesson_booking.accept(self, slot, accepter) + 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) + 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 + else + # nothing to do with the original booking (since we are not changing all times), so we update just ourself + time = update_next_available_time # XXX: week offset as 0? This *could* still be in the past. But the user is approving it. So do we just trust them and get out of their way? + + if time.nil? + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") + 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) + UserMailer.student_lesson_accepted(self, message, slot).deliver + UserMailer.teacher_lesson_accepted(self, message, slot).deliver + end + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + puts("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + response = self + raise ActiveRecord::Rollback + end + end + end + response + end + + def counter(params) + response = self + LessonSession.transaction do + proposer = params[:proposer] + slot = params[:slot] + message = params[:message] + + update_all = slot.update_all || !lesson_booking.recurring + self.countering = true + slot.proposer = proposer + slot.lesson_session = self + slot.message = message + self.lesson_booking_slots << slot + self.countered_slot = slot + self.countered_lesson = self + self.status = STATUS_COUNTERED + if !update_all + 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 + end + else + response = self + raise ActiveRecord::Rollback + end + + + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + Notification.send_lesson_message('counter', self, slot.is_teacher_created?) + end + + response + end + + + # teacher accepts the lesson + def cancel(params) + response = self + LessonSession.transaction do + + canceler = params[:canceler] + other = canceler == teacher ? student : teacher + message = params[:message] + + if params[:update_all].present? + update_all = params[:update_all] + else + update_all = !lesson_booking.recurring + 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 + end + else + response = self + raise ActiveRecord::Rollback + end + + end + + response + end + + def description(lesson_booking) + lesson_booking.lesson_package_type.description(lesson_booking) + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + 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 new file mode 100644 index 000000000..d1883a77d --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -0,0 +1,307 @@ +module JamRuby + class LessonSessionAnalyser + + SUCCESS = 'success' + SESSION_ONGOING = 'session_ongoing' + THRESHOLD_MET = 'threshold_met' + WAITED_CORRECTLY = 'waited_correctly' + MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently + MINIMUM_TIME_NOT_MET = 'mininum_time_not_met' + LATE_CANCELLATION = 'late_cancellation' + + TEACHER_FAULT = 'teacher_fault' + STUDENT_FAULT = 'student_fault' + BOTH_FAULT = 'both_fault' + + STUDENT_NOT_THERE_WHEN_JOINED = 'student_not_there_when_joined' + JOINED_LATE = 'did_not_join_on_time' + NO_SHOW = 'no_show' + + + # what are the potential results? + + # bill: true/false + + # teacher: 'no_show' + # teacher: 'late' + # teacher: 'early_leave' + # teacher: 'waited_correctly' + # teacher: 'late_cancellation' + + # student: 'no_show' + # student: 'late' + # student: 'early_leave' + # student: 'minimum_time_not_met' + # student: 'threshold_met' + + + # reason: 'session_ongoing' + # reason: 'success' + # reason: 'student_fault' + # reason: 'teacher_fault' + # reason: 'both_fault' + + + def self.analyse(lesson_session) + reason = nil + teacher = nil + student = nil + bill = false + + music_session = lesson_session.music_session + + student_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.student.id) + teacher_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.teacher.id) + + # create ranges from music session user history + all_student_ranges = time_ranges(student_histories) + all_teacher_ranges = time_ranges(teacher_histories) + + # flatten ranges into non-overlapping ranges to simplifly logic + student_ranges = merge_overlapping_ranges(all_student_ranges) + 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? + 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 + bill = true + reason = SUCCESS + elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] + # if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes; + # and if either: + + if student_analysis[:no_show] + # the student no-showed entirely, then we bill the student. + student = NO_SHOW + bill = true + + elsif student_analysis[:joined_late] + # the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student + student = JOINED_LATE + bill = true + end + end + + end + + if reason.nil? + if student + reason = STUDENT_FAULT + elsif teacher + reason = TEACHER_FAULT + end + end + + { + reason: reason, + teacher: teacher, + student: student, + bill: bill, + student_ranges: student_ranges, + teacher_ranges: teacher_ranges, + intersecting: intersecting, + student_analysis: student_analysis, + teacher_analysis: teacher_analysis, + together_analysis: together_analysis, + } + end + + def self.annotate_timeline(lesson_session, analysis, ranges) + start = lesson_session.scheduled_start + end + + def self.intersecting_ranges(ranges_a, ranges_b) + intersections = [] + ranges_a.each do |range_a| + ranges_b.each do |range_b| + intersection = intersect(range_a, range_b) + intersections << intersection if intersection + end + end + + merge_overlapping_ranges(intersections) + end + + # needs to add to joined_on_time + # joined_on_time bool + # waited_correctly bool + # no_show bool + # joined_late bool + # minimum_time_met bool + # present_at_end bool + + + def self.analyse_intersection(lesson_session, ranges) + start = lesson_session.scheduled_start + planned_duration_seconds = lesson_session.duration * 60 + end_time = start + planned_duration_seconds + + join_start_boundary_begin = start + join_start_boundary_end = start + (APP_CONFIG.lesson_join_time_window_minutes * 60) + + wait_boundary_begin = start + wait_boundary_end = start + (APP_CONFIG.lesson_wait_time_window_minutes * 60) + + initial_join_window = Range.new(join_start_boundary_begin, join_start_boundary_end) + initial_wait_window = Range.new(wait_boundary_begin, wait_boundary_end) + session_window = Range.new(start, end_time) + + # let's see how much time they spent together, irrespective of scheduled time + # and also, based on scheduled time + total = 0 + + in_scheduled_time = 0 + in_wait_window_time = 0 + + # the initial time joined in the initial 'waiting window' + initial_join_in_scheduled_time = nil + + # the amount of time spent in the initial 'waiting window' + initial_wait_time_in_scheduled_time = 0 + + last_wait_time_out = nil + + joined_on_time = false + waited_correctly = false + no_show = true + joined_late = false + joined_in_wait_window = false + ranges.each do |range| + time = range.end - range.begin + + total += time + + in_session_range = intersect(range, session_window) + in_join_window_range = intersect(range, initial_join_window) + in_wait_window_range = intersect(range, initial_wait_window) + + if in_session_range + in_scheduled_time += in_session_range.end - in_session_range.begin + no_show = false + end + + if in_join_window_range + if initial_join_in_scheduled_time.nil? + initial_join_in_scheduled_time = in_join_window_range.begin + end + joined_on_time = true + end + + if in_wait_window_range + in_wait_window_time += in_wait_window_range.end - in_wait_window_range.begin + last_wait_time_out = range.end + joined_in_wait_window = true + end + end + + if joined_in_wait_window && !joined_on_time + joined_late = true + end + + if last_wait_time_out && last_wait_time_out > wait_boundary_end + last_wait_time_out = wait_boundary_end + end + + initial_waiting_time_pct = nil + potential_waiting_time = nil + + # let's see if this person was hanging around for the bulk of this waiting window (to rule out someone coming/going very fast, trying to miss someone) + if last_wait_time_out && initial_join_in_scheduled_time + total_in_waiting_time = 0 + potential_waiting_range = Range.new(initial_join_in_scheduled_time, last_wait_time_out) + ranges.each do |range| + in_waiting = intersect(potential_waiting_range, range) + if in_waiting + total_in_waiting_time += in_waiting.end - in_waiting.begin + end + end + + potential_waiting_time = last_wait_time_out - initial_join_in_scheduled_time + initial_waiting_time_pct = total_in_waiting_time.to_f / potential_waiting_time.to_f + + # finally with all this stuff calculated, we can check: + # 1) did they wait a solid % of time between the time they joined, and left, during the initial 10 minute waiting window? + # 2) did they + if (initial_waiting_time_pct >= APP_CONFIG.wait_time_window_pct) && + (last_wait_time_out >= (wait_boundary_end - (APP_CONFIG.end_of_wait_window_forgiveness_minutes * 60))) + waited_correctly = true + end + end + + + # percentage computation of time spent during the session time + in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f + + joined_on_time = joined_on_time + { + total_time: total, + session_time: in_scheduled_time, + session_pct: in_scheduled_percentage, + joined_on_time: joined_on_time, + waited_correctly: waited_correctly, + no_show: no_show, + joined_late: joined_late, + initial_join_in_scheduled_time: initial_join_in_scheduled_time, + last_wait_time_out: last_wait_time_out, + in_wait_window_time: in_wait_window_time, + initial_waiting_pct: initial_waiting_time_pct, + potential_waiting_time: potential_waiting_time + } + end + + def self.intersect(a, b) + min, max = a.first, a.exclude_end? ? a.max : a.last + other_min, other_max = b.first, b.exclude_end? ? b.max : b.last + + new_min = a === other_min ? other_min : b === min ? min : nil + new_max = a === other_max ? other_max : b === max ? max : nil + + new_min && new_max ? Range.new(new_min, new_max) : nil + end + + def self.time_ranges(histories) + ranges = [] + histories.each do |history| + ranges << history.range + end + ranges + end + + def self.ranges_overlap?(a, b) + a.include?(b.begin) || b.include?(a.begin) + end + + def self.merge_ranges(a, b) + [a.begin, b.begin].min..[a.end, b.end].max + end + + def self.merge_overlapping_ranges(ranges) + ranges.sort_by(&:begin).inject([]) do |ranges, range| + if !ranges.empty? && ranges_overlap?(ranges.last, range) + ranges[0...-1] + [merge_ranges(ranges.last, range)] + else + ranges + [range] + end + end + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb new file mode 100644 index 000000000..068721a2b --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb @@ -0,0 +1,28 @@ +module JamRuby + class LessonSessionMonthlyPrice + + # calculate the price for a given month + def self.price(lesson_booking, start_day) + + 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) + + result = nil + if times.length == 0 + result = 0 + elsif times.length == 1 + result = (lesson_booking.booked_price * 0.25).round(2) + elsif times.length == 2 + result = (lesson_booking.booked_price * 0.50).round(2) + elsif times.length == 3 + result = (lesson_booking.booked_price * 0.75).round(2) + else + result = lesson_booking.booked_price + end + + result + end + end +end + diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 68c387d47..d63c798db 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -21,6 +21,7 @@ module JamRuby CREATE_TYPE_RSVP = 'rsvp' CREATE_TYPE_IMMEDIATE = 'immediately' CREATE_TYPE_QUICK_START = 'quick-start' + CREATE_TYPE_LESSON = 'lesson' attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed @@ -31,12 +32,10 @@ module JamRuby self.primary_key = 'id' belongs_to :creator,:class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :music_session_histories - belongs_to :band, :class_name => 'JamRuby::Band', :foreign_key => :band_id, :inverse_of => :music_sessions - belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id - belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession" has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id" @@ -190,6 +189,10 @@ module JamRuby end end + def is_lesson? + !!lesson_session + end + def grouped_tracks tracks = [] self.music_session_user_histories.each do |msuh| @@ -375,6 +378,11 @@ module JamRuby ms.create_type = options[:create_type] ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp] ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone] + ms.scheduled_start = options[:scheduled_start] if options[:scheduled_start] + if options[:lesson_session] + ms.lesson_session = options[:lesson_session] + end + ms.save @@ -414,6 +422,10 @@ module JamRuby case ms.create_type when CREATE_TYPE_RSVP, CREATE_TYPE_SCHEDULE_FUTURE Notification.send_scheduled_session_invitation(ms, receiver) + when CREATE_TYPE_LESSON + if ms.lesson_session.is_active? + Notification.send_jamclass_invitation_teacher(ms, receiver) + end else Notification.send_session_invitation(receiver, user, ms.id) end @@ -932,7 +944,7 @@ SQL # with_timezone = FALSE # Thursday, July 10 - 10:00pm # this should be in a helper - def pretty_scheduled_start(with_timezone) + def pretty_scheduled_start(with_timezone = true) if scheduled_start && scheduled_duration start_time = scheduled_start 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 c8b8c0834..2e7bd25d4 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -10,8 +10,8 @@ module JamRuby attr_accessible :max_concurrent_connections, :session_removed_at, :rating validates_inclusion_of :rating, :in => -1..1, :allow_nil => true - belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :music_session_user_histories - belongs_to :music_session, :class_name => "MusicSession", :foreign_key => "music_session_id" + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => :user_id, :inverse_of => :music_session_user_histories + belongs_to :music_session, :class_name => "MusicSession", :foreign_key => :music_session_id def self.latest_history(client_id) self.where(:client_id => client_id) @@ -25,6 +25,10 @@ module JamRuby user.name end + def range + Range.new(created_at, session_removed_at || Time.now) + end + def music_session @msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id) end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 6a0821f40..d47ecff00 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -12,6 +12,7 @@ module JamRuby belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id" belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id" belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id" + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession", :foreign_key => "lesson_session_id" belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id" belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id" @@ -62,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, :band => band, :session => session}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -132,6 +133,8 @@ module JamRuby user = options[:user] band = options[:band] session = options[:session] + purpose = options[:purpose] + student_directed = options[:student_directed] name, band_name = "" unless user.nil? @@ -249,8 +252,37 @@ module JamRuby when NotificationTypes::BAND_INVITATION_ACCEPTED return "#{name} has accepted your band invitation to join #{band_name}." + when NotificationTypes::LESSON_MESSAGE + notification_msg = 'Lesson Changed' + + if purpose == 'requested' + notification_msg = 'You have received a lesson request' + elsif purpose == 'accept' + notification_msg = 'Your lesson request is confirmed!' + elsif purpose == 'declined' + notification_msg = "We're sorry your lesson request has been declined." + elsif purpose == 'canceled' + notification_msg = "Your lesson request has been canceled." + elsif purpose == 'counter' + if student_directed + notification_msg = "Instructor has proposed a different time for your lesson." + else + notification_msg = "Student has proposed a different time for your lesson." + end + elsif purpose == 'reschedule' + 'A lesson reschedule has been requested' + end + return notification_msg + + when NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + if student_directed + "You have been scheduled to take a JamClass with #{user.name}." + else + "You have been scheduled to teach a JamClass to #{user.name}" + end + else - return "" + return description end end @@ -372,6 +404,49 @@ module JamRuby end end + def send_lesson_message(purpose, lesson_session, student_directed) + + notification = Notification.new + notification.description = NotificationTypes::LESSON_MESSAGE + notification.student_directed = student_directed + + if !student_directed + notification.source_user_id = lesson_session.student.id + notification.target_user_id = lesson_session.teacher.id + else + notification.source_user_id = lesson_session.teacher.id + notification.target_user_id = lesson_session.student.id + end + + notification.purpose = purpose + 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.message = notification_msg + + notification.save + + # receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at + message = @@message_factory.lesson_message( + notification.target_user.id, + notification.source_user.resolved_photo_url, + notification.source_user.name, + notification.source_user.id, + notification_msg, + notification.id, + notification.session_id, + notification.created_date, + notification.student_directed, + notification.purpose, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(notification.target_user.id, message) + + end + def send_new_band_follower(follower, band) band.band_musicians.each.each do |bm| @@ -644,44 +719,129 @@ module JamRuby end end - def send_scheduled_session_invitation(music_session, user) - + def send_jamclass_invitation_teacher(music_session, user) return if music_session.nil? || user.nil? - target_user = user - source_user = music_session.creator + teacher = target_user = user + student = source_user = music_session.creator + + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: student, student_directed: false}) notification = Notification.new - notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION notification.source_user_id = source_user.id notification.target_user_id = target_user.id notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = false + #notification.message = notification_msg notification.save - notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) + if target_user.online - msg = @@message_factory.scheduled_session_invitation( - target_user.id, - music_session.id, - source_user.photo_url, - notification_msg, - music_session.name, - music_session.pretty_scheduled_start(false), - notification.id, - notification.created_date + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session.id ) @@mq_router.publish_to_user(target_user.id, msg) end begin - UserMailer.scheduled_session_invitation(target_user, 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_SESSION_INVITATION email to user #{target_user.email} #{e}") + @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{music_session.lesson_session.teacher.email} #{e}") end end + def send_student_jamclass_invitation(music_session, user) + return if music_session.nil? || user.nil? + + student = target_user = user + teacher = source_user = music_session.lesson_session.teacher + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: teacher, student_directed: true}) + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = true + #notification.message = notification_msg + notification.save + + if target_user.online + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + 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 + + end + + + def send_scheduled_session_invitation(music_session, user) + + return if music_session.nil? || user.nil? + + target_user = user + source_user = music_session.creator + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.save + + notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) + + if target_user.online + msg = @@message_factory.scheduled_session_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + UserMailer.scheduled_session_invitation(target_user, notification_msg, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_INVITATION email to user #{target_user.email} #{e}") + end + end + def send_scheduled_session_rsvp(music_session, user, instruments) return if music_session.nil? || user.nil? diff --git a/ruby/lib/jam_ruby/models/review.rb b/ruby/lib/jam_ruby/models/review.rb index 7c98619d9..4fe6e755b 100644 --- a/ruby/lib/jam_ruby/models/review.rb +++ b/ruby/lib/jam_ruby/models/review.rb @@ -20,6 +20,17 @@ module JamRuby after_save :reduce + def self.create(params) + review = Review.new + review.target = params[:target] + review.user = params[:user] + review.rating = params[:rating] + review.description = params[:description] + review.target_type = params[:target].class.to_s + review.save + review + end + def self.index(options={}) if options.key?(:include_deleted) arel = Review.all diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 0490b1afa..5cbd4f9d7 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -4,6 +4,7 @@ module JamRuby class Sale < ActiveRecord::Base JAMTRACK_SALE = 'jamtrack' + LESSON_SALE = 'lesson' SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' @@ -35,12 +36,12 @@ module JamRuby # 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} + 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 } + {query: query, next_page: next_page} end end @@ -207,6 +208,116 @@ module JamRuby free && non_free end + def self.purchase_test_drive(current_user, booking = nil) + self.purchase_lesson(current_user, booking, LessonPackageType.test_drive) + end + + def self.purchase_normal(current_user, booking) + self.purchase_lesson(current_user, booking, LessonPackageType.single, booking.lesson_sessions[0]) + end + + # this is easy to make generic, but right now, it just purchases lessons + def self.purchase_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false) + stripe_charge = nil + sale = nil + purchase = nil + # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it + Sale.transaction(:requires_new => true) do + + sale = create_lesson_sale(current_user) + + if sale.valid? + + sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + + price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) + + if price_info[:purchase] && price_info[:purchase].errors.any? + purchase = price_info[:purchase] + raise ActiveRecord::Rollback + end + + if !sale_line_item.valid? + raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" + end + # sale.source = 'stripe' + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.stripe_charge_id = price_info[:charge_id] + sale.save + stripe_charge = price_info[:charge] + purchase = price_info[:purchase] + else + # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. + raise "invalid sale object" + end + + end + {sale: sale, stripe_charge: stripe_charge, purchase: purchase} + end + + def self.charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false) + if lesson_package_purchase + target = lesson_package_purchase + elsif lesson_session + target = lesson_session + else + target = lesson_package_type + end + + current_user.sync_stripe_customer + + purchase = lesson_package_purchase + purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil? + + if purchase.errors.any? + price_info = {} + price_info[:purchase] = purchase + return price_info + end + + if lesson_session + lesson_session.lesson_package_purchase_id = purchase.id + lesson_session.save! + end + + subtotal_in_cents = purchase.price_in_cents + + tax_percent = 0 + if current_user.stripe_zip_code + lookup = ZipCodes.identify(current_user.stripe_zip_code) + if lookup && lookup[:state_code] == 'TX' + tax_percent = 0.0825 + end + end + + tax_in_cents = (subtotal_in_cents * tax_percent).round + total_in_cents = subtotal_in_cents + tax_in_cents + + stripe_charge = Stripe::Charge.create( + :amount => total_in_cents, + :currency => "usd", + :customer => current_user.stripe_customer_id, + :description => target.stripe_description(lesson_booking) + ) + + sale_line_item.lesson_package_purchase = purchase + sale_line_item.save + + price_info = {} + price_info[:subtotal_in_cents] = subtotal_in_cents + price_info[:tax_in_cents] = tax_in_cents + price_info[:total_in_cents] = total_in_cents + price_info[:currency] = 'USD' + price_info[:charge_id] = stripe_charge.id + price_info[:charge] = stripe_charge + price_info[:purchase] = purchase + price_info + end + + # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned def self.order_jam_tracks(current_user, shopping_carts) @@ -365,7 +476,6 @@ module JamRuby end - if account # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack adjustments = shopping_cart.create_adjustment_attributes(current_user) @@ -481,6 +591,10 @@ module JamRuby sale_type == JAMTRACK_SALE end + def is_lesson_sale? + sale_type == LESSON_SALE + end + def self.create_jam_track_sale(user, sale_source=nil) sale = Sale.new sale.user = user @@ -491,6 +605,15 @@ module JamRuby sale end + def self.create_lesson_sale(user) + sale = Sale.new + sale.user = user + sale.sale_type = LESSON_SALE # gift cards and jam tracks are sold with this type of sale + sale.order_total = 0 + sale.save + sale + end + # this checks just jamtrack sales appropriately def self.check_integrity_of_jam_track_sales Sale.select([:total, :voided]).find_by_sql( diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index e105f966b..61e753bbe 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -5,15 +5,22 @@ module JamRuby JAMCLOUD = 'JamCloud' JAMTRACK = 'JamTrack' GIFTCARD = 'GiftCardType' + LESSON = 'LessonPackageType' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' belongs_to :gift_card, class_name: 'JamRuby::GiftCard' + belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + + # deprecated; use affiliate_distribution !! belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + has_many :affiliate_distributions, class_name: 'JamRuby::AffiliateDistribution' + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' - validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -81,8 +88,28 @@ module JamRuby line_item end - def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) - product_info = shopping_cart.product_info + # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible + def self.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + teacher = lesson_booking.teacher if lesson_booking + shopping_cart = ShoppingCart.create(current_user, lesson_package_type, 1) + line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil, lesson_booking) + + if lesson_booking + # if the teacher came from an affiliate, this is our chance to account for that (the student's affiliate status was accounted for in create_from_shopping_cart) + referral_info = teacher.should_attribute_sale?(shopping_cart, lesson_booking) + + if referral_info + line_item.affiliate_distributions << AffiliateDistribution.create(teacher.affiliate_referral, referral_info[:fee_in_cents], line_item) + line_item.save! + end + end + + shopping_cart.destroy + line_item + end + + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil) + product_info = shopping_cart.product_info(instance) sale.order_total = sale.order_total + product_info[:real_price] @@ -101,9 +128,10 @@ module JamRuby # determine if we need to associate this sale with a partner user = shopping_cart.user - referral_info = user.should_attribute_sale?(shopping_cart) + referral_info = user.should_attribute_sale?(shopping_cart, instance) if referral_info + sale_line_item.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], sale_line_item) sale_line_item.affiliate_referral = user.affiliate_referral sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] end diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb new file mode 100644 index 000000000..1c573d778 --- /dev/null +++ b/ruby/lib/jam_ruby/models/school.rb @@ -0,0 +1,107 @@ +module JamRuby + class School < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:name] + + + # the school will handle all communication with students when setting up a session + SCHEDULING_COMM_SCHOOL = 'school' + # the teacher will handle all communication with students when setting up a session + SCHEDULING_COMM_TEACHER = 'teacher' + SCHEDULING_COMMS = [ SCHEDULING_COMM_SCHOOL, SCHEDULING_COMM_TEACHER ] + + attr_accessor :updating_avatar + attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection + + belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_school + has_many :students, class_name: ::JamRuby::User + has_many :teachers, class_name: ::JamRuby::Teacher + has_many :school_invitations, class_name: 'JamRuby::SchoolInvitation' + + validates :user, presence: true + validates :enabled, inclusion: {in: [true, false]} + validates :scheduling_communication, inclusion: {in: SCHEDULING_COMMS} + validates :correspondence_email, email: true, allow_blank: true + validate :validate_avatar_info + + before_save :stringify_avatar_info, :if => :updating_avatar + + def update_from_params(params) + self.name = params[:name] if params[:name].present? + self.scheduling_communication = params[:scheduling_communication] if params[:scheduling_communication].present? + self.correspondence_email = params[:correspondence_email] if params[:correspondence_email].present? + self.save + end + + def owner + user + end + + def validate_avatar_info + if updating_avatar + # we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io) + errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil? + errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil? + errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil? + end + end + + def escape_filename(path) + dir = File.dirname(path) + file = File.basename(path) + "#{dir}/#{ERB::Util.url_encode(file)}" + end + + def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket) + self.updating_avatar = true + + cropped_s3_path = cropped_fpfile["key"] + cropped_large_s3_path = cropped_large_fpfile["key"] + + self.update_attributes( + :original_fpfile => original_fpfile, + :cropped_fpfile => cropped_fpfile, + :cropped_large_fpfile => cropped_large_fpfile, + :cropped_s3_path => cropped_s3_path, + :cropped_large_s3_path => cropped_large_s3_path, + :crop_selection => crop_selection, + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) + end + + def delete_avatar(aws_bucket) + + User.transaction do + + unless self.cropped_s3_path.nil? + S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg') + S3Util.delete(aws_bucket, self.cropped_s3_path) + S3Util.delete(aws_bucket, self.cropped_large_s3_path) + end + + return self.update_attributes( + :original_fpfile => nil, + :cropped_fpfile => nil, + :cropped_large_fpfile => nil, + :cropped_s3_path => nil, + :cropped_large_s3_path => nil, + :photo_url => nil, + :crop_selection => nil, + :large_photo_url => nil + ) + end + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? + self.crop_selection = crop_selection.to_json if !crop_selection.nil? + end + end +end diff --git a/ruby/lib/jam_ruby/models/school_invitation.rb b/ruby/lib/jam_ruby/models/school_invitation.rb new file mode 100644 index 000000000..beea9fd3c --- /dev/null +++ b/ruby/lib/jam_ruby/models/school_invitation.rb @@ -0,0 +1,100 @@ +module JamRuby + class SchoolInvitation < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:note] + + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :school, class_name: ::JamRuby::School + + validates :school, presence: true + validates :email, email: true + validates :invitation_code, presence: true + validates :as_teacher, inclusion: {in: [true, false]} + validates :accepted, inclusion: {in: [true, false]} + validates :first_name, presence: true + validates :last_name, presence: true + validate :school_has_name, on: :create + + before_validation(on: :create) do + self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? + end + + def school_has_name + if school && school.name.blank? + errors.add(:school, "must have name") + end + end + + def self.index(school, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = SchoolInvitation.where(school_id: school.id) + query = query.includes([:user, :school]) + query = query.order('created_at') + query = query.where(as_teacher: params[:as_teacher]) + query = query.where(accepted:false) + + + 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 self.create(current_user, specified_school, params) + + invitation = SchoolInvitation.new + invitation.school = specified_school + invitation.as_teacher = params[:as_teacher] + invitation.email = params[:email] + invitation.first_name = params[:first_name] + invitation.last_name = params[:last_name] + + if invitation.save + invitation.send_invitation + end + invitation + end + + + def send_invitation + if as_teacher + UserMailer.invite_school_teacher(self).deliver + else + UserMailer.invite_school_student(self).deliver + end + end + def generate_signup_url + if as_teacher + "#{APP_CONFIG.external_root_url}/school/#{school.id}/teacher?invitation_code=#{self.invitation_code}" + else + "#{APP_CONFIG.external_root_url}/school/#{school.id}/student?invitation_code=#{self.invitation_code}" + end + end + + def delete + self.destroy + end + + def resend + send_invitation + end + + + + end +end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index fb531c2bd..5c3543f70 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -26,9 +26,13 @@ module JamRuby default_scope order('created_at DESC') - def product_info + def product_info(instance = nil) product = self.cart_product - {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil? + data = {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil? + if data && instance + data.merge!(instance.product_info) + end + data end # multiply quantity by price @@ -115,6 +119,10 @@ module JamRuby cart_type == GiftCardType::PRODUCT_TYPE end + def is_lesson? + cart_type == LessonPackageType::PRODUCT_TYPE + end + # returns an array of adjustments for the shopping cart def create_adjustment_attributes(current_user) raise "not a jam track or gift card" unless is_jam_track? || is_gift_card? diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index c95353d6c..69640f35a 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -13,6 +13,16 @@ module JamRuby validates :redirect_location, length: {maximum: 1000} validates :want_jamblaster, inclusion: {in: [nil, true, false]} + def self.create_redirect(user, options = {}) + hint = SignupHint.new + hint.user = user + hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) + hint.want_jamblaster = false + hint.expires_at = 2.days.from_now + hint.save + hint + end + def self.refresh_by_anoymous_user(anonymous_user, options = {}) hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id) @@ -34,5 +44,13 @@ module JamRuby SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all end + def self.most_recent_redirect(user, default) + hint = SignupHint.where(user_id: user.id).order('created_at desc').first + if hint + hint.redirect_location + else + default + end + end end end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index 0daca4373..a6982e944 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -13,8 +13,11 @@ module JamRuby has_many :experiences_education, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'education'} has_many :experiences_award, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'award'} has_many :reviews, :class_name => "JamRuby::Review", as: :target - has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target - has_one :user, :class_name => 'JamRuby::User' + has_many :lesson_sessions, :class_name => "JamRuby::LessonSession" + has_many :lesson_package_purchases, :class_name => "JamRuby::LessonPackagePurchase" + has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target + has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers validates :user, :presence => true validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction @@ -52,6 +55,9 @@ module JamRuby query = User.joins(:teacher) + # only show teachers with background check set and ready for session set to true + query = query.where('teachers.ready_for_session_at IS NOT NULL') + instruments = params[:instruments] if instruments && !instruments.blank? && instruments.length > 0 query = query.joins("inner JOIN teachers_instruments AS tinst ON tinst.teacher_id = teachers.id") @@ -87,7 +93,7 @@ module JamRuby end years_teaching = params[:years_teaching].to_i - if years_teaching && years_teaching > 0 + if params[:years_teaching] && years_teaching > 0 query = query.where('years_teaching >= ?', years_teaching) end @@ -95,20 +101,20 @@ module JamRuby teaches_intermediate = params[:teaches_intermediate] teaches_advanced = params[:teaches_advanced] - if teaches_beginner || teaches_intermediate || teaches_advanced + if teaches_beginner.present? || teaches_intermediate.present? || teaches_advanced.present? clause = '' - if teaches_beginner + if teaches_beginner == true clause << 'teaches_beginner = true' end - if teaches_intermediate + if teaches_intermediate == true if clause.length > 0 clause << ' OR ' end clause << 'teaches_intermediate = true' end - if teaches_advanced + if teaches_advanced == true if clause.length > 0 clause << ' OR ' end @@ -118,7 +124,7 @@ module JamRuby end student_age = params[:student_age].to_i - if student_age && student_age > 0 + if params[:student_age] && student_age > 0 query = query.where("teaches_age_lower <= ? AND (CASE WHEN teaches_age_upper = 0 THEN true ELSE teaches_age_upper >= ? END)", student_age, student_age) end @@ -150,13 +156,11 @@ module JamRuby end teacher = user.teacher - teacher ||= user.build_teacher() + teacher ||= Teacher.new teacher.user = user teacher.website = params[:website] if params.key?(:website) teacher.biography = params[:biography] if params.key?(:biography) - teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video) - teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video) teacher.years_teaching = params[:years_teaching] if params.key?(:years_teaching) teacher.years_playing = params[:years_playing] if params.key?(:years_playing) @@ -186,6 +190,7 @@ 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.school_id = params[:school_id] if params.key?(:school_id) # Many-to-many relations: if params.key?(:genres) @@ -243,6 +248,21 @@ module JamRuby teacher end + def booking_price(lesson_length, single) + price = nil + if single + price = self["price_per_lesson_#{lesson_length}_cents"] + else + price = self["price_per_month_#{lesson_length}_cents"] + end + + if !price.nil? + price / 100.0 + else + price + end + end + def offer_pricing unless prices_per_lesson.present? || prices_per_month.present? errors.add(:offer_pricing, "Must choose to price per lesson or per month") diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb new file mode 100644 index 000000000..585d8e61b --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -0,0 +1,76 @@ +module JamRuby + class TeacherDistribution < ActiveRecord::Base + + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: "teacher_id" + 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" + + validates :teacher, presence: true + validates :amount_in_cents, presence: true + + def self.create_for_lesson(lesson_session) + distribution = create(lesson_session) + distribution.lesson_session = lesson_session + distribution + end + + def self.create_for_lesson_package_purchase(lesson_package_purchase) + distribution = create(lesson_package_purchase) + distribution.lesson_package_purchase = lesson_package_purchase + distribution + end + + def self.create(target) + distribution = TeacherDistribution.new + distribution.teacher = target.teacher + distribution.ready = false + distribution.distributed = false + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents + distribution + end + def amount + amount_in_cents / 100.0 + end + + def student + if lesson_session + lesson_session.student + else + lesson_package_purchase.student + end + end + + def month_name + lesson_package_purchase.month_name + end + + def is_test_drive? + lesson_session && lesson_session.is_test_drive? + end + + def is_normal? + lesson_session && !lesson_session.is_test_drive? + end + + def is_monthly? + !lesson_package_purchase.nil? + end + + 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 + else + "Monthly session for the month of #{lesson_package_purchase.month_name} with #{lesson_package_purchase.lesson_booking.student.name}" + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_intent.rb b/ruby/lib/jam_ruby/models/teacher_intent.rb new file mode 100644 index 000000000..333cf5afe --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_intent.rb @@ -0,0 +1,24 @@ +module JamRuby + class TeacherIntent < ActiveRecord::Base + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :teacher, class_name: ::JamRuby::Teacher + + validates :user, presence: true + validates :teacher, presence: true + validates :intent, presence: true + + def self.create(user, teacher, intent) + teacher_intent = TeacherIntent.new + teacher_intent.user = user + teacher_intent.teacher = teacher + teacher_intent.intent = intent + teacher_intent.save + teacher_intent + end + + def self.recent_test_drive(user) + TeacherIntent.where(intent: 'book-test-drive').where(user_id: user.id).where('created_at > ?', Date.today - 30).order('created_at DESC').first + end + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb new file mode 100644 index 000000000..3ee7ee162 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -0,0 +1,112 @@ +module JamRuby + class TeacherPayment < ActiveRecord::Base + + 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" + + + def self.hourly_check + teacher_payments + end + + def teacher_distributions + [teacher_distribution] + end + + def self.pending_teacher_payments + User.select(['users.id']).joins(:teacher).joins(:teacher_distributions).where('teachers.stripe_account_id IS NOT NULL').where('teacher_distributions.distributed = false').where('teacher_distributions.ready = true').uniq + end + + def self.teacher_payments + pending_teacher_payments.each do |row| + teacher = User.find(row['id']) + + TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).each do |distribution| + payment = TeacherPayment.charge(teacher) + if payment.nil? || !payment.teacher_payment_charge.billed + break + end + end + + end + end + + def amount + amount_in_cents / 100.0 + end + + def is_card_declined? + teacher_payment_charge.is_card_declined? + end + + def is_card_expired? + teacher_payment_charge.is_card_expired? + end + + def last_billed_at_date + teacher_payment_charge.last_billed_at_date + end + def charge_retry_hours + 24 + end + + def calculate_teacher_fee + if teacher_distribution.is_test_drive? + 0 + else + (amount_in_cents * 0.28).round + end + 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) + payment = TeacherPayment.joins(:teacher_payment_charge).where('teacher_payments.teacher_id = ?', teacher.id).where('charges.billed = false').order(:created_at).first + if payment.nil? + payment = TeacherPayment.new + payment.teacher = teacher + else + payment = TeacherPayment.find(payment.id) + end + + if payment.teacher_distribution.nil? + teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).order(:created_at).first + if teacher_distribution.nil? + return + end + payment.teacher_distribution = teacher_distribution + end + + + payment.amount_in_cents = payment.teacher_distribution.amount_in_cents + payment.fee_in_cents = payment.calculate_teacher_fee + + if payment.teacher_payment_charge.nil? + charge = TeacherPaymentCharge.new + charge.amount_in_cents = payment.amount_in_cents + charge.fee_in_cents = payment.fee_in_cents + charge.teacher_payment = payment + payment.teacher_payment_charge = charge + # charge.save! + else + charge = payment.teacher_payment_charge + charge.amount_in_cents = payment.amount_in_cents + charge.fee_in_cents = payment.fee_in_cents + charge.save! + end + + payment.save! + + payment.teacher_payment_charge.charge + + if payment.teacher_payment_charge.billed + payment.teacher_distribution.distributed = true + payment.teacher_distribution.save! + end + payment + end + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb new file mode 100644 index 000000000..e54bc5330 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -0,0 +1,53 @@ +module JamRuby + class TeacherPaymentCharge < Charge + + has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.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? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + UserMailer.teacher_distribution_done(teacher_payment) + end + + def do_send_unable_charge + UserMailer.teacher_distribution_fail(teacher_payment) + end + + def construct_description + teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 50f350c01..bfd92fdae 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -170,6 +170,13 @@ module JamRuby has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight", :foreign_key => "user_id" has_many :purchased_jam_tracks, :through => :jam_track_rights, :class_name => "JamRuby::JamTrack", :source => :jam_track, :order => :created_at + # lessons + has_many :lesson_purchases, :class_name => "JamRuby::LessonPackagePurchase", :foreign_key => "user_id", inverse_of: :user + has_many :student_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "user_id", inverse_of: :user + 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 + # Shopping carts has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" @@ -187,10 +194,14 @@ module JamRuby has_one :musician_search, :class_name => 'JamRuby::MusicianSearch' has_one :band_search, :class_name => 'JamRuby::BandSearch' - belongs_to :teacher, :class_name => 'JamRuby::Teacher' + belongs_to :teacher, :class_name => 'JamRuby::Teacher', foreign_key: :teacher_id has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" + has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students + has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user + has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users @@ -200,7 +211,6 @@ module JamRuby validates :first_name, length: {maximum: 50}, no_profanity: true validates :last_name, length: {maximum: 50}, no_profanity: true - validates :last_name, length: {maximum: 50}, no_profanity: true validates :biography, length: {maximum: 4000}, no_profanity: true validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email @@ -211,6 +221,8 @@ module JamRuby validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false} validates :reuse_card, :inclusion => {:in => [true, false]} + validates :is_a_student, :inclusion => {:in => [true, false]} + validates :is_a_teacher, :inclusion => {:in => [true, false]} validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} validates :gifted_jamtracks, presence: true, :numericality => {:less_than_or_equal_to => 100} validates :subscribe_email, :inclusion => {:in => [nil, true, false]} @@ -1076,12 +1088,25 @@ module JamRuby gift_card = options[:gift_card] student = options[:student] teacher = options[:teacher] + school_invitation_code = options[:school_invitation_code] + school_id = options[:school_id] user = User.new user.validate_instruments = true UserManager.active_record_transaction do |user_manager| - user.first_name = first_name - user.last_name = last_name + + if school_invitation_code + school_invitation = SchoolInvitation.find_by_invitation_code(school_invitation_code) + if school_invitation + first_name ||= school_invitation.first_name + last_name ||= school_invitation.last_name + school_invitation.accepted = true + school_invitation.save + end + end + + user.first_name = first_name if first_name.present? + user.last_name = last_name if last_name.present? user.email = email user.subscribe_email = true user.terms_of_service = terms_of_service @@ -1095,7 +1120,15 @@ module JamRuby end user.musician = !!musician - + if school_id.present? + if user.is_a_student + 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' + user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Teaches for #{school_name}", school_id: school_id) + end + end # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. # The right thing would be to have this null @@ -1277,10 +1310,21 @@ module JamRuby user.save end if affiliate_referral_id.present? - # don't send an signup email if email is already confirmed - if user.email_confirmed + + + if user.is_a_student + UserMailer.student_welcome_message(user).deliver + end + + if user.is_a_teacher + UserMailer.teacher_welcome_message(user).deliver + end + + if !user.is_a_teacher && !user.is_a_student UserMailer.welcome_message(user).deliver - else + end + + if !user.email_confirmed # any errors here should also rollback the transaction; that's OK. If emails aren't going to be delivered, # it's already a really bad situation; make user signup again UserMailer.confirm_email(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token)).deliver @@ -1798,9 +1842,15 @@ module JamRuby options end - def should_attribute_sale?(shopping_cart) + def should_attribute_sale?(shopping_cart, instance = nil) + + if shopping_cart.is_lesson? && shopping_cart.cart_product.is_test_drive? + # never attribute test drives + return false + end + if affiliate_referral - referral_info = affiliate_referral.should_attribute_sale?(shopping_cart) + referral_info = affiliate_referral.should_attribute_sale?(shopping_cart, self, instance) else false end @@ -1821,11 +1871,215 @@ module JamRuby using_free_credit end + def has_stored_credit_card? + stored_credit_card + end + + def has_free_lessons? + remaining_free_lessons > 0 + end + + def can_book_free_lesson? + has_free_lessons? && has_stored_credit_card? + 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 + end + + def has_test_drives? + remaining_test_drives > 0 + end + + def has_unprocessed_test_drives? + !unprocessed_test_drive.nil? + end + + def has_requested_test_drive?(teacher = nil) + !requested_test_drive(teacher).nil? + end + + def fetch_stripe_customer + Stripe::Customer.retrieve(stripe_customer_id) + end + + # if the user already has a stripe customer, then keep it synced. otherwise create it + def sync_stripe_customer + if self.stripe_customer_id + # we already have a customer for this user; re-use it + customer = fetch_stripe_customer + + if customer.email.nil? || customer.email.downcase != email.downcase + customer.email = email + customer.save + end + else + customer = Stripe::Customer.create( + :description => admin_url, + :source => stripe_token, + :email => email) + end + self.stripe_customer_id = customer.id + User.where(id: id).update_all(stripe_customer_id: customer.id) + + customer + end + def card_approved(token, zip) + + approved_booking = nil + User.transaction do + self.stripe_token = token if token + self.stripe_zip_code = zip if zip + customer = sync_stripe_customer + 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 + end + end + end + approved_booking + end + + def update_name(name) + if name.blank? + self.first_name = '' + self.last_name = '' + else + bits = name.split + if bits.length == 1 + self.first_name = '' + self.last_name = bits[0].strip + elsif bits.length == 2 + self.first_name = bits[0].strip + self.last_name = bits[1].strip + else + self.first_name = bits[0].strip + self.last_name = bits[1..-1].join(' ') + end + end + self.save + end + + def payment_update(params) + booking = nil + test_drive = nil + normal = nil + intent = nil + purchase = nil + User.transaction do + + if params[:name].present? + if !self.update_name(params[:name]) + return nil + end + end + + booking = card_approved(params[:token], params[:zip]) + if params[:test_drive] + self.reload + result = Sale.purchase_test_drive(self, booking) + test_drive = result[:sale] + purchase = result[:purchase] + elsif params[:normal] + self.reload + end + + intent = TeacherIntent.recent_test_drive(self) + end + + {lesson: booking, test_drive: test_drive, intent:intent, purchase: purchase} + end + + def requested_test_drive(teacher = nil) + query = LessonBooking.requested(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE) + if teacher + query = query.where(teacher_id: teacher.id) + end + query.first + end + + def unprocessed_test_drive + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE).first + end + + def unprocessed_normal_lesson + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first + end + + def most_recent_test_drive_purchase + lesson_purchases.where(lesson_package_type_id: LessonPackageType.test_drive.id).order('created_at desc').first + end + + def test_drive_succeeded(lesson_session) + if self.remaining_test_drives <= 0 + UserMailer.student_test_drive_lesson_done(lesson_session).deliver + UserMailer.teacher_lesson_completed(lesson_session).deliver + else + UserMailer.student_test_drive_lesson_completed(lesson_session).deliver + UserMailer.teacher_lesson_completed(lesson_session).deliver + end + 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 + end + + def used_test_drives + 4 - remaining_test_drives + end + + def has_rated_teacher(teacher) + if teacher.is_a?(JamRuby::User) + teacher = teacher.teacher + end + Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s).count > 0 + end + + def has_rated_student(student) + Review.where(target_id: student.id).where(target_type: "JamRuby::User").count > 0 + end + + def teacher_profile_url + "#{APP_CONFIG.external_root_url}/client#/profile/teacher/#{id}" + end + + def ratings_url + "#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/teacher/#{id}" + end + + def student_ratings_url + "#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/#{id}" + end + + def self.search_url + "#{APP_CONFIG.external_root_url}/client#/jamclass/searchOptions" + end + + def recent_test_drive_teachers + User.select('distinct on (users.id) users.*').joins(taught_lessons: :music_session).where('lesson_sessions.lesson_type = ?', LessonSession::LESSON_TYPE_TEST_DRIVE).where('music_sessions.user_id = ?', id).where('lesson_sessions.created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago) + end + def mark_session_ready self.ready_for_session_at = Time.now self.save! end + def has_booked_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).count > 0 + end + + def has_booked_test_drive_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).test_drive.count > 0 + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb index f4c1523eb..9710e7082 100644 --- a/ruby/lib/jam_ruby/models/user_authorization.rb +++ b/ruby/lib/jam_ruby/models/user_authorization.rb @@ -12,6 +12,10 @@ module JamRuby validates_uniqueness_of :uid, scope: :provider # token, secret, token_expiration can be missing + def is_active? + token_expiration && token_expiration < Time.now + end + def self.refreshing_google_auth(user) auth = self.where(:user_id => user.id) .where(:provider => 'google_login') diff --git a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb new file mode 100644 index 000000000..d9097d3c6 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb @@ -0,0 +1,18 @@ +module JamRuby + class HourlyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_hourly_job + @@log = Logging.logger[HourlyJob] + + def self.perform + @@log.debug("waking up") + + LessonBooking.hourly_check + LessonSession.hourly_check + TeacherPayment.hourly_check + + @@log.debug("done") + end + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 9e62c3500..73c0a9fad 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -21,6 +21,9 @@ FactoryGirl.define do reuse_card true has_redeemable_jamtrack true gifted_jamtracks 0 + remaining_free_lessons 1 + remaining_test_drives 0 + stored_credit_card false #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -92,10 +95,17 @@ FactoryGirl.define do connection = FactoryGirl.create(:connection, :user => user, :music_session => active_music_session) end end + factory :teacher_user do + after(:create) do |user, evaluator| + teacher = FactoryGirl.create(:teacher, user: user, price_per_lesson_60_cents: 3000, price_per_month_60_cents: 3000) + end + end end factory :teacher, :class => JamRuby::Teacher do association :user, factory: :user + price_per_lesson_60_cents 3000 + price_per_month_60_cents 3000 end factory :musician_instrument, :class => JamRuby::MusicianInstrument do @@ -146,6 +156,9 @@ FactoryGirl.define do end factory :music_session, :class => JamRuby::MusicSession do + ignore do + student nil + end sequence(:name) { |n| "Music Session #{n}" } sequence(:description) { |n| "Music Session Description #{n}" } fan_chat true @@ -893,7 +906,6 @@ FactoryGirl.define do association :user, factory: :user sequence(:serial_no ) { |n| "serial_no#{n}" } - sequence(:vtoken ) { |n| "vtoken#{n}" } sequence(:client_id ) { |n| "client_id#{n}" } end @@ -906,6 +918,116 @@ FactoryGirl.define do sequence(:sibling_key ) { |n| "sibling_key#{n}" } end + factory :school, class: 'JamRuby::School' do + association :user, factory: :user + sequence(:name) {|n| "Dat Music School"} + enabled true + scheduling_communication 'teacher' + end + + factory :school_invitation, class: 'JamRuby::SchoolInvitation' do + association :school, factory: :school + note "hey come in in" + as_teacher true + sequence(:email) {|n| "school_person#{n}@example.com"} + sequence(:first_name) {|n| "FirstName"} + sequence(:last_name) {|n| "LastName"} + accepted false + end + + factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do + factory :lesson_booking_slot_single do + slot_type 'single' + preferred_day Date.today + 3 + day_of_week nil + hour 12 + minute 30 + timezone 'UTC' + end + + factory :lesson_booking_slot_recurring do + slot_type 'recurring' + preferred_day nil + day_of_week 0 + hour 12 + minute 30 + timezone 'UTC' + end + end + + factory :lesson_booking, class: 'JamRuby::LessonBooking' do + association :user, factory: :user + association :teacher, factory: :teacher_user + card_presumed_ok false + sent_notices false + recurring false + lesson_length 30 + lesson_type JamRuby::LessonBooking::LESSON_TYPE_FREE + payment_style JamRuby::LessonBooking::PAYMENT_STYLE_ELSEWHERE + description "Oh my goodness!" + status JamRuby::LessonBooking::STATUS_REQUESTED + before(:create) do |lesson_booking, evaluator| + lesson_booking.lesson_booking_slots = [FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking), + FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking)] + end + #lesson_booking_slots [FactoryGirl.build(:lesson_booking_slot_single), FactoryGirl.build(:lesson_booking_slot_single)] + end + + factory :lesson_package_purchase, class: "JamRuby::LessonPackagePurchase" do + lesson_package_type { JamRuby::LessonPackageType.single } + association :user, factory: :user + association :teacher, factory: :teacher_user + price 30.00 + + factory :test_drive_purchase do + lesson_package_type { JamRuby::LessonPackageType.test_drive } + association :lesson_booking, factory: :lesson_booking + price 49.99 + end + end + + factory :lesson_session, class: 'JamRuby::LessonSession' do + + ignore do + student nil + end + + music_session {FactoryGirl.create(:music_session, creator: student)} + lesson_booking {FactoryGirl.create(:lesson_booking, user: student, teacher: teacher)} + association :teacher, factory: :teacher_user + lesson_type JamRuby::LessonSession::LESSON_TYPE_SINGLE + duration 30 + booked_price 49.99 + status JamRuby::LessonSession::STATUS_REQUESTED + #teacher_complete true + #student_complete true + end + + factory :charge, class: 'JamRuby::Charge' do + type 'JamRuby::Charge' + amount_in_cents 1000 + end + + factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do + type 'JamRuby::TeacherPaymentCharge' + end + + + factory :teacher_payment, class: 'JamRuby::TeacherPayment' do + association :teacher, factory: :teacher_user + association :teacher_payment_charge, factory: :teacher_payment_charge + amount_in_cents 1000 + end + + # you gotta pass either lesson_session or lesson_package_purchase for this to make sense + factory :teacher_distribution, class: 'JamRuby::TeacherDistribution' do + association :teacher, factory: :teacher_user + association :teacher_payment, factory: :teacher_payment + ready false + amount_in_cents 1000 + end + + factory :ip_blacklist, class: "JamRuby::IpBlacklist" do remote_ip '1.1.1.1' end diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index 694323832..b16efb2d0 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -568,6 +568,5 @@ describe ConnectionManager, no_transaction: true do # connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS) # connection.errors.size.should == 0 end - 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 new file mode 100644 index 000000000..505f97d85 --- /dev/null +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -0,0 +1,215 @@ +require 'spec_helper' + +describe "Monthly Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + + after {Timecop.return} + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + it "works" do + + # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on + Timecop.travel(Date.new(2016, 3, 20)) + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## 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.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### 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 + notification.message.should eql "Student has proposed a different time for your lesson." + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 4 + 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 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + 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 + notification.message.should eql "Your lesson request is confirmed!" + + # let user pay for it + LessonBooking.hourly_check + + 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 + teacher_distribution = lesson_purchase.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + 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 + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + teacher_distribution.reload + teacher_distribution.distributed.should be_true + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql 3000 + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + + + + # 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! + + + 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 "monthly recurring lesson 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.amount_charged.should eql 0.0 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be false + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 1 # one for student + end +end diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb new file mode 100644 index 000000000..24b85c2ad --- /dev/null +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -0,0 +1,414 @@ +require 'spec_helper' + +describe "Normal Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + describe "stripe mocked" do + before { StripeMock.start + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + after { StripeMock.stop } + after {Timecop.return} + + it "bill failure" do + + # 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.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## 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.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql booking.default_slot + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + 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 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + + StripeMock.prepare_card_error(:card_declined) + + lesson_session.lesson_payment_charge.billing_attempts.should eql 0 + user.lesson_purchases.length.should eql 0 + 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 + lesson_session.billing_attempts.should eql 1 + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + LessonBooking.hourly_check + + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_false + teacher_distribution.distributed.should be_false + + + # let's reattempt right away; this should have no effect because we only try to bill once per 24 hours + + 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 + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 1).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:expired_card) + + 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 + lesson_session.billing_attempts.should eql 2 + lesson_session.billing_error_reason.should eql 'card_expired' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 24 + 2).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:processing_error) + + 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 + lesson_session.billing_attempts.should eql 3 + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + + Timecop.freeze((24 + 24 + 24 + 3).hours.from_now) + StripeMock.clear_errors + + # finally the user will get billed! + LessonSession.hourly_check + + + 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.post_processed.should be_true + LessonPaymentCharge.count.should eql 2 + + 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 + lesson_session.billing_attempts.should eql 4 + if lesson_session.billing_error_detail + lesson_session.billing_error_detail + end + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql true + user.reload + user.lesson_purchases.length.should eql 1 + + + LessonBooking.hourly_check + payment.reload + payment.amount_in_cents.should eql 3248 + payment.fee_in_cents.should eql 0 + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + 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.reload + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + TeacherPayment.count.should eql 1 + teacher_distribution.reload + teacher_distribution.distributed.should be_true + + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql 3000 + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + + end + end + + + it "works" do + + # 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.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## 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.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + 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 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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! + + + 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 + end +end diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb new file mode 100644 index 000000000..3946eecdc --- /dev/null +++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe "Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## 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.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14, update_all: true) + 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 + #notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16, update_all: true) + 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}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 6 + 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 6 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + 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! + + + 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 + end +end diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb new file mode 100644 index 000000000..e9545cfbe --- /dev/null +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -0,0 +1,219 @@ +require 'spec_helper' + +describe "TestDrive Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + + it "works" do + + # 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 + booking.card_presumed_ok.should be_false + booking.user.should eql user + 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 + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + test_drive = result[:test_drive] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + test_drive.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.reload + + + 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_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 + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + 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 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + 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 + notification.message.should eql "Your lesson request is confirmed!" + + + # 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! + + + + 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 + lesson_session.billed.should be false + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billing_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.lesson_package_type.is_test_drive?.should be true + user.reload + user.remaining_test_drives.should eql 3 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + LessonBooking.hourly_check + LessonSession.hourly_check + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + TeacherPayment.count.should eql 1 + + lesson_session.reload + purchase.reload + + purchase.teacher_distribution.should be_nil + + teacher_payment = TeacherPayment.first + teacher_payment.amount_in_cents.should eql 1000 + teacher_payment.fee_in_cents.should eql 0 + teacher_payment.teacher.should eql teacher_user + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + + teacher_payment.teacher_payment_charge.amount_in_cents.should eql 1000 + teacher_payment.teacher_payment_charge.fee_in_cents.should eql 0 + + end +end diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 3c23bdc73..99fd536a5 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -343,6 +343,8 @@ describe AffiliatePartner do freebie_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) freebie_sale.affiliate_referral_fee_in_cents.should eq(0) freebie_sale.created_at = Date.new(2015, 1, 1) + freebie_sale.affiliate_distributions.first.created_at = freebie_sale.created_at + freebie_sale.affiliate_distributions.first.save! freebie_sale.save! @@ -363,6 +365,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -378,6 +382,9 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -393,6 +400,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -424,6 +433,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2014, 12, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -439,6 +450,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! real_sale_later = real_sale AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) @@ -455,6 +468,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 3, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -466,6 +481,9 @@ describe AffiliatePartner do real_sale.affiliate_refunded_at = Date.new(2015, 3, 1) real_sale.affiliate_refunded = true real_sale.save! + real_sale.affiliate_distributions.first.affiliate_refunded_at = real_sale.affiliate_refunded_at + real_sale.affiliate_distributions.first.affiliate_refunded = real_sale.affiliate_refunded + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -485,6 +503,9 @@ describe AffiliatePartner do real_sale_later.affiliate_refunded_at = Date.new(2015, 7, 1) real_sale_later.affiliate_refunded = true real_sale_later.save! + real_sale_later.affiliate_distributions.first.affiliate_refunded_at = real_sale_later.affiliate_refunded_at + real_sale_later.affiliate_distributions.first.affiliate_refunded = real_sale_later.affiliate_refunded + real_sale_later.affiliate_distributions.first.save! AffiliatePartner.total_quarters(2015, 1) payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) payment.due_amount_in_cents.should eq(20) @@ -556,6 +577,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 1)) AffiliateQuarterlyPayment.count.should eq(3) @@ -600,6 +623,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 2)) quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) diff --git a/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb new file mode 100644 index 000000000..fb1019b97 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBookingSlot do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single, timezone: 'US/Pacific', hour: 12) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring, timezone: 'US/Pacific', hour: 12) } + + + before do + + end + + after do + Timecop.return + end + + describe "next_day" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + it "same day of week" do + + @now.wday.should eql 1 + # let's make it match + lesson_booking_slot_recurring1.day_of_week = @now.wday + + (@now.to_date).should eql lesson_booking_slot_recurring1.next_day + end + + it "day of week is tomorrow" do + + # make the slot be today + 1 (Tuesday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday + 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 1) + end + + it "day of week is yesterday" do + # make the slot be today + 1 (Sunday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday - 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 6) + end + + it "day of week is two day ago" do + # make the slot be today + 1 (Saturday in this case) + lesson_booking_slot_recurring1.day_of_week = 6 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 5) + end + end + + describe "scheduled_time" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time OK" do + time = lesson_booking_slot_single1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK 1 week out" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(1) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end + + + describe "scheduled_times" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time session OK" do + times = lesson_booking_slot_single1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + + it "creates a recurring time session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + 3 + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 3 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time session OK (minimum time hit)" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates 2 recurring times session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(2, @now.to_date + 2) + times.length.should eq 2 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + time = times[1] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 14 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb new file mode 100644 index 000000000..ac64a0f3a --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -0,0 +1,899 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBooking do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + describe "suspend!" do + it "should set status as well as update status of all associated lesson_sessions" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.lesson_sessions[0].accept({accepter: teacher_user, message: "got it", slot: booking.lesson_booking_slots[0].id}) + booking.reload + booking.active.should eql true + booking.suspend! + booking.errors.any?.should be false + booking.reload + booking.active.should eql false + booking.status.should eql LessonBooking::STATUS_SUSPENDED + booking.lesson_sessions.count.should eql 2 + booking.lesson_sessions.each do |lesson_session| + lesson_session.status.should eql LessonBooking::STATUS_SUSPENDED + end + end + end + + describe "bill_monthlies" do + after do + Timecop.return + end + + it "empty" do + LessonBooking.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 0 + end + + it "one" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should be_nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + # don't advance time, but nothing should happen because last_billing time hasn't elapsed + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 2 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + user.card_approved(create_stripe_token, '78759') + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 3 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "advances to next month" do + user.card_approved(create_stripe_token, '78759') + user.save! + + day = Date.new(2016, 1, 20) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + LessonSession.count.should eql 2 + + purchase = LessonPackagePurchase.first + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + + day = Date.new(2016, 1, 27) + time = day.to_time + Timecop.freeze(time) + + LessonBooking.schedule_upcoming_lessons + LessonSession.count.should eql 3 + LessonPackagePurchase.count.should eql 1 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 2 + + purchase = LessonPackagePurchase.order(:month).last + purchase.month.should eql 2 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "will suspend after enough tries" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.billing_attempts.should eql 1 + booking.is_suspended?.should be_false + purchase.billed.should be false + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 2 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 3 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 4 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 5 + booking.reload + booking.is_suspended?.should be_true + purchase.billed.should be false + + # now that it's suspended, let's unsuspend it + user.card_approved(create_stripe_token, '78759') + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + purchase.bill_monthly(true) + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 6 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + booking.reload + booking.is_suspended?.should be_false + end + + it "missed meetings deduct on next month" do + # TODO. Discuss with David a little more + end + end + + describe "billable_monthlies" do + before do + Timecop.return + end + + it "empty" do + LessonBooking.billable_monthlies(Time.now).count.should eql 0 + end + + it "one" do + time = Date.new(2016, 1, 1) + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + now = Time.now + billables = LessonBooking.billable_monthlies(now) + billables.all.should eql [booking] + LessonPackagePurchase.where(lesson_booking_id: booking.id).count.should eql 0 + # to make this billable monthly go away, we will need to create one LessonPackagePurchase; one for this month (because it's only one we have lessons in) + # and further, mark them both as post_processed + + package = LessonPackagePurchase.create(user, booking, LessonPackageType.single, 2016, 1) + + LessonBooking.billable_monthlies(now).count.should eql 1 + + package.post_processed = true + package.save! + + LessonBooking.billable_monthlies(now).count.should eql 0 + end + end + + describe "predicted_times_for_month" do + after do + Timecop.return + end + it "fills up month" do + + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 5 + times[0].to_date.should eql (jan1) + times[1].to_date.should eql (Date.new(next_year, 1, 8)) + times[2].to_date.should eql (Date.new(next_year, 1, 15)) + times[3].to_date.should eql (Date.new(next_year, 1, 22)) + times[4].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "fills up partial month" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan15) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 3 + times[0].to_date.should eql (Date.new(next_year, 1, 15)) + times[1].to_date.should eql (Date.new(next_year, 1, 22)) + times[2].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "let's assume JamKazam is messed up for a few weeks" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + # book a session on jan1 + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + # but don't run the computation of times per month for weeks + Timecop.freeze(Date.new(next_year, 1, 23)) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 2 + times[0].to_date.should eql (Date.new(next_year, 1, 1)) + times[1].to_date.should eql (Date.new(next_year, 1, 29)) + end + 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 + + user.has_free_lessons?.should be_true + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without free lesson" 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 + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:user].should eq ["have no remaining free lessons"] + + 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.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["must have two times specified"] + end + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_free(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_test_drive" do + it "works" do + user.stored_credit_card = true + user.save! + booking = LessonBooking.book_test_drive(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_TEST_DRIVE + 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 1 + user.remaining_test_drives.should eq 0 + end + + it "allows long message to flow through chat" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without remaining test drives" do + + user.stored_credit_card = true + user.save! + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + ChatMessage.count.should eq 1 + user.reload + user.remaining_test_drives.should eql 0 + + 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"] + + ChatMessage.count.should eq 1 + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_normal" do + it "works" do + 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.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_SINGLE + booking.recurring.should eq false + booking.lesson_length.should eq 60 + 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.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + it "works with recurring slots" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + 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_WEEKLY + booking.recurring.should eq true + booking.lesson_length.should eq 60 + 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.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + 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.should_not be_nil + chat_message.message.should eq booking.description + end + + it "does not prevent user without remaining test drives" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevents user without free lesson" do + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevent user without a stored credit card" do + user.stored_credit_card = false + user.save! + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + booking.card_presumed_ok.should eq false + booking.sent_notices.should eq false + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_recurring1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:day_of_week].should eq ["must be specified"] + end + end + + describe "find_bookings_needing_sessions" do + after do + Timecop.return + end + it "can detect missing lesson and schedules it 1 week out" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 2 + + lesson_session = booking.lesson_sessions[0] + Timecop.freeze(lesson_session.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 3 + + # check that all the times make sense + + lesson1 = booking.lesson_sessions[0] + lesson2 = booking.lesson_sessions[1] + lesson3 = booking.lesson_sessions[2] + lesson1.music_session.scheduled_start.to_i.should eql (lesson2.music_session.scheduled_start.to_i - (60 * 60 * 24 * 7)) + lesson1.music_session.scheduled_start.to_i.should eql (lesson3.music_session.scheduled_start.to_i - (60 * 60 * 24 * 14)) + + Timecop.freeze(lesson2.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 4 + + # check that all the times make sense + + lesson4 = booking.lesson_sessions[3] + lesson1.music_session.scheduled_start.to_i.should eql (lesson4.music_session.scheduled_start.to_i - (60 * 60 * 24 * 21)) + end + + it "ignores non-recurring" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 1 + booking.accepter.should eql user + end + end + + describe "canceling" do + + after do + Timecop.return + end + + it "single session gets canceled before accepted" do + + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # avoid 24 hour problem + + UserMailer.deliveries.clear + 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.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 1 + end + + it "single session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + 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 + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 2 + end + + it "recurring session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.active.should eql false + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.active.should eql true + + UserMailer.deliveries.clear + 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.canceler.should eql user + booking.reload + booking.active.should eql true + booking.status.should eql LessonSession::STATUS_APPROVED + booking.canceler.should be_nil + UserMailer.deliveries.length.should eql 2 + end + + it "recurring booking gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + 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 + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + booking.canceler.should eql user + UserMailer.deliveries.length.should eql 2 + end + end + describe "rescheduling" do + + after do + Timecop.return + end + it "non-recurring, accepted with new slot" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.counter_slot.should eql counter + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + booking.reload + booking.accepter.should eql teacher_user + booking.counter_slot.should be_nil + end + + it "recurring" do + Timecop.freeze(Time.new(2016, 03, 4, 5, 0, 0)) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 2) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # to help scoot out the 'created_at' of the lessons + Timecop.freeze(Time.now + 10) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.status.should eql LessonSession::STATUS_APPROVED + + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session2 = booking.lesson_sessions.order(:created_at)[1] + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + + # now it's approved, we have 2 sessions that are not yet completed with a time + + # we should be able to reschedule just one of the lessons + counter2 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 4, update_all: false) + lesson_session.counter({proposer: user, slot: counter2, message: 'ACtually, let\'s do this instead for just this one'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix just this one', slot: counter2.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) # STILL ORIGINAL COUNTER! + + # we should be able to reschedule all of the lessons + + + counter3 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 5, update_all: true) + lesson_session.counter({proposer: user, slot: counter3, message: 'ACtually, let\'s do this instead for just this one... again'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + booking.reload + booking.counter_slot.should eql counter3 + + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix all of them', slot: counter3.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.counter_slot.should be_nil + lesson_session.reload + lesson_session2.reload + lesson_session.created_at.should be < lesson_session2.created_at + + lesson_session.scheduled_start.should eql counter3.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter3.scheduled_time(1 ) + + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb new file mode 100644 index 000000000..5d542f0c4 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe LessonPackagePurchase do + + let(:user) {FactoryGirl.create(:user)} + let(:lesson_booking) {FactoryGirl.create(:lesson_booking)} + +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb new file mode 100644 index 000000000..fcd2816b5 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb @@ -0,0 +1,257 @@ +require 'spec_helper' + +describe LessonSessionAnalyser do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 1) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + describe "analyse" do + it "teacher joined on time, waited, student no show" 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: teacher, history: music_session, created_at: start, session_removed_at: end_time) + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis[:student].should eql LessonSessionAnalyser::NO_SHOW + analysis[:bill].should be true + + 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 joined late" 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) + late_start = start + (7 * 60) + 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! + + + 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 + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be true + 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 (4 * 60.to_f) + end + + it "together 5 minutes" do + + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: start, session_removed_at: end_time) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SUCCESS + analysis[:bill].should be true + + teacher = analysis[:teacher_analysis] + teacher[:joined_on_time].should be true + teacher[:waited_correctly].should be true + teacher[:initial_waiting_pct].should eql 1.0 + teacher[:potential_waiting_time].should eql 600.0 + + together = analysis[:together_analysis] + together[:session_time].should eql 1800.0 + end + end + + describe "intersecting_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + it "empty" do + LessonSessionAnalyser.intersecting_ranges([], []).should eql [] + end + + it "one specified, other empty" do + + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([], [Range.new(uh1Begin, uh1End)]).should eql [] + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], []).should eql [] + end + + it "both identical" do + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh1End)] + end + + it "one intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 1 + uh2End = start + 3 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [Range.new(uh2Begin, uh2End)] + end + + it "overlapping intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + -1 + uh2End = start + 2 + + uh3Begin = start + 3 + uh3End = start + 5 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh2Begin, uh2End), Range.new(uh3Begin, uh3End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh2End), Range.new(uh3Begin, uh1End)] + end + + it "no overlap" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 5 + uh2End = start + 6 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [] + end + end + + describe "merge_overlapping_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + + it "empty" do + LessonSessionAnalyser.merge_overlapping_ranges([]).should eql [] + end + + it "one item" do + uh1Begin = start + uh1End = start + 1 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + + ranges = LessonSessionAnalyser.time_ranges [uh1] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two identical items" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = uh1Begin + uh2End = uh1End + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two separate times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 3 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End), Range.new(uh2Begin, uh2End)] + end + + it "two overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh2End)] + end + + it "three overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh3Begin = start + 0.1 + uh3End = start + 6 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + uh3 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh3Begin, session_removed_at: uh3End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2, uh3] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh3End)] + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb new file mode 100644 index 000000000..0f254d5e7 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe LessonSessionMonthlyPrice do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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] } + + describe "price" do + after do + Timecop.return + end + + it "start of the month" do + + jan1 = Date.new(2016, 1, 1) + Timecop.freeze(jan1) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + booking.booked_price.should eql(LessonSessionMonthlyPrice.price(booking, jan1)) + end + + it "middle of the month" do + + jan17 = Date.new(2016, 1, 17) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.75).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which has a last billable day based on slot" do + + jan17 = Date.new(2016, 1, 31) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.25).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which is not a last billable days" do + + feb29 = Date.new(2016, 2, -1) + Timecop.freeze(feb29) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.0).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, feb29)) + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_spec.rb new file mode 100644 index 000000000..e1470bdff --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe LessonSession do + + let(:user) {FactoryGirl.create(:user, stored_credit_card: false, 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_session) {lesson_booking.lesson_sessions[0]} + + describe "accept" do + it "can accept" do + + 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 + + query = LessonSession.index(user)[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user))[:query] + query.length.should eq 0 + end + + it "finds single lesson as teacher" do + + # just sanity check that the lesson_session Factory is doing what it should + lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user + lesson_session.lesson_booking.teacher.should eql teacher + + query = LessonSession.index(teacher, {as_teacher: true})[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user), {as_teacher: true})[:query] + query.length.should eq 0 + end + end +end diff --git a/ruby/spec/jam_ruby/models/review_spec.rb b/ruby/spec/jam_ruby/models/review_spec.rb index 7a5851948..3deed6f3b 100644 --- a/ruby/spec/jam_ruby/models/review_spec.rb +++ b/ruby/spec/jam_ruby/models/review_spec.rb @@ -16,7 +16,7 @@ describe Review do context "validates review" do it "blank target" do - review = Review.create() + review = Review.create({}) review.valid?.should be_false review.errors[:target].should == ["can't be blank"] end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index fc89145cb..bc278b7e2 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' describe Sale do - let(:user) {FactoryGirl.create(:user)} - let(:user2) {FactoryGirl.create(:user)} - let(:jam_track) {FactoryGirl.create(:jam_track)} - let(:jam_track2) {FactoryGirl.create(:jam_track)} - let(:jam_track3) {FactoryGirl.create(:jam_track)} - let(:gift_card) {GiftCardType.jam_track_5} + let(:user) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:gift_card) { GiftCardType.jam_track_5 } def assert_free_line_item(sale_line_item, jamtrack) sale_line_item.recurly_tax_in_cents.should be_nil @@ -63,7 +63,7 @@ describe Sale do describe "place_order" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } let(:jamtrack) { FactoryGirl.create(:jam_track) } let(:jamtrack2) { FactoryGirl.create(:jam_track) } let(:jamtrack3) { FactoryGirl.create(:jam_track) } @@ -265,7 +265,6 @@ describe Sale do user.gifted_jamtracks.should eq(1) - # OK! Now make a second purchase; this time, buy one free, one not free shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true @@ -567,6 +566,291 @@ describe Sale do end end + describe "lessons" do + + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + 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(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + + describe "single" do + it "can succeed" do + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # 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! + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 3000 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + 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 + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 + end + + it "affiliate" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # 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! + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + puts lesson_session.billing_error_reason + puts lesson_session.billing_error_detail + user.reload + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 248 + sale.recurly_total_in_cents.should eql 3248 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + 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 + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 2 + affiliate_partner.reload + affiliate_partner2.reload + affiliate_partner.affiliate_distributions.count.should eql 1 + affiliate_partner2.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner2_distribution = affiliate_partner2.affiliate_distributions.first + partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item + partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner.lesson_rate).round + partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner2.lesson_rate).round + end + + it "book recurring, monthly" do + + end + + end + describe "purchase_test_drive" do + it "can succeed" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', test_drive: true}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 4999 + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + 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 + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with tax" do + #user.remaining_test_drives = 0 + #user.save! + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + user.reload + user.remaining_test_drives.should eql 0 + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + 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 + + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + lesson_purchase = booking.lesson_sessions[0].lesson_package_purchase + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions[0].accept({message: "got it", slot: booking.lesson_booking_slots[0].id}) + user.reload + user.remaining_test_drives.should eql 3 + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with no booking; just intent" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + 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 + end + + it "will reject second test drive purchase" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_false + + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_true + purchase.errors[:user].should eq ["can not buy test drive right now because you have already purchased it within the last year"] + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + + + end + end + end + describe "check_integrity_of_jam_track_sales" do let(:user) { FactoryGirl.create(:user) } diff --git a/ruby/spec/jam_ruby/models/school_invitation_spec.rb b/ruby/spec/jam_ruby/models/school_invitation_spec.rb new file mode 100644 index 000000000..b851d434f --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_invitation_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe SchoolInvitation do + + let(:school) {FactoryGirl.create(:school)} + + it "created by factory" do + FactoryGirl.create(:school_invitation) + end + + it "created by method" do + SchoolInvitation.create(school.user, school, {as_teacher: true, first_name: "Bobby", last_name: "Jimes", email: "somewhere@jamkazam.com"}) + end + + describe "index" do + it "works" do + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: true) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: false) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 1 + + end + + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/school_spec.rb b/ruby/spec/jam_ruby/models/school_spec.rb new file mode 100644 index 000000000..0eac36175 --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe School do + + it "created by factory" do + FactoryGirl.create(:school) + end + + it "has correct associations" do + school = FactoryGirl.create(:school) + + school.should eql school.user.owned_school + + student = FactoryGirl.create(:user, school: school) + teacher = FactoryGirl.create(:teacher, school: school) + + school.reload + school.students.should eql [student] + school.teachers.should eql [teacher] + + student.school.should eql school + teacher.school.should eql school + end + + it "updates" do + school = FactoryGirl.create(:school) + school.update_from_params({name: 'hahah', scheduling_communication: 'school', correspondence_email: 'bobby@jamkazam.com'}) + school.errors.any?.should be_false + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb new file mode 100644 index 000000000..58ac4d16f --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -0,0 +1,293 @@ +require 'spec_helper' + +describe TeacherPayment do + + let(:user) { FactoryGirl.create(:user) } + 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(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) } + let(:teacher2) { FactoryGirl.create(:user, teacher: teacher_obj2) } + 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)} + + describe "pending_teacher_payments" do + + it "empty" do + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "one distribution" do + test_drive_distribution.touch + + 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 + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + test_drive_distribution2.ready = true + test_drive_distribution2.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 2 + payment_user_ids = payments.map(&:id) + payment_user_ids.include? teacher.id + payment_user_ids.include? teacher2.id + end + end + + describe "teacher_payments" do + it "empty" do + TeacherPayment.teacher_payments + end + + it "charges test drive" do + test_drive_distribution.touch + + test_drive_distribution.ready = true + test_drive_distribution.save! + + TeacherPayment.teacher_payments + + test_drive_distribution.reload + test_drive_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = test_drive_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 + 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 0 + 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 + charge.application_fee.should eql nil + + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "charges normal" do + normal_distribution.touch + + 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 + + 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 + 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 + charge.application_fee.should include("fee_") + end + + it "charges multiple" do + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + 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 2 + + 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 + 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 + charge.application_fee.should include("fee_") + + test_drive_distribution.reload + payment = test_drive_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 + 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 0 + 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 + charge.application_fee.should be_nil + end + + + + describe "stripe mocked" do + before { StripeMock.start } + after { StripeMock.stop; Timecop.return } + + it "failed payment, then success" do + StripeMock.prepare_card_error(:card_declined) + + normal_distribution.touch + 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) + + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.teacher_payment_charge.billed.should be_false + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + StripeMock.clear_errors + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 2 + + payment = normal_distribution.teacher_payment + + 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 + + test_drive_distribution.reload + payment = test_drive_distribution.teacher_payment + + 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 0 + 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 + end + + end +end diff --git a/ruby/spec/jam_ruby/models/teacher_spec.rb b/ruby/spec/jam_ruby/models/teacher_spec.rb index 7ec2cf2e5..3b0c305d0 100644 --- a/ruby/spec/jam_ruby/models/teacher_spec.rb +++ b/ruby/spec/jam_ruby/models/teacher_spec.rb @@ -14,14 +14,14 @@ describe Teacher do describe "index" do it "no params" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil)[:query] teachers.length.should eq 1 teachers[0].should eq(teacher.user) end it "instruments" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query] teachers.length.should eq 0 @@ -39,7 +39,7 @@ describe Teacher do end it "subjects" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {subjects: ['music-theory']})[:query] teachers.length.should eq 0 @@ -51,7 +51,7 @@ describe Teacher do end it "genres" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {genres: ['ambient']})[:query] teachers.length.should eq 0 @@ -64,7 +64,7 @@ describe Teacher do it "languages" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {languages: ['EN']})[:query] teachers.length.should eq 0 @@ -76,7 +76,7 @@ describe Teacher do end it "country" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {country: 'DO'})[:query] teachers.length.should eq 0 @@ -88,7 +88,7 @@ describe Teacher do end it "region" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {region: 'HE'})[:query] teachers.length.should eq 0 @@ -100,7 +100,7 @@ describe Teacher do end it "years_teaching" do - teacher = FactoryGirl.create(:teacher, years_teaching: 5) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now, years_teaching: 5) teachers = Teacher.index(nil, {years_teaching: 10})[:query] teachers.length.should eq 0 @@ -110,7 +110,7 @@ describe Teacher do end it "teaches beginner/intermediate/advanced" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {teaches_beginner: true})[:query] teachers.length.should eq 0 @@ -133,7 +133,7 @@ describe Teacher do end it "student_age" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {student_age: 5})[:query] teachers.length.should eq 1 @@ -213,6 +213,7 @@ describe Teacher do years_playing: 12 ) teacher.should_not be_nil + teacher.user.should eql user teacher.errors.should be_empty teacher.id.should_not be_nil t = Teacher.find(teacher.id) @@ -347,6 +348,11 @@ describe Teacher do end describe "validates" do + it "barebones" do + teacher = Teacher.save_teacher(user, validate_introduction: true, biography: "Teaches for dat school") + teacher.errors.should be_empty + + end it "introduction" do teacher = Teacher.save_teacher( user, diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 89abe6e16..36e3cf949 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -97,7 +97,7 @@ describe User do it { should be_admin } end - + describe "when first name is not present" do before { @user.first_name = " " } @@ -136,14 +136,13 @@ describe User do end - describe "first or last name cant have profanity" do it "should not let the first name have profanity" do @user.first_name = "fuck you" @user.save @user.should_not be_valid end - + it "should not let the last name have profanity" do @user.last_name = "fuck you" @user.save @@ -226,7 +225,7 @@ describe User do User.authenticate(@user.email, "newpassword").should_not be_nil UserMailer.deliveries.length.should == 1 end - + it "setting a new password should fail if old one doesnt match" do @user.set_password("wrongold", "newpassword", "newpassword") @user.errors.any?.should be_true @@ -240,7 +239,7 @@ describe User do @user.errors[:password].length.should == 1 UserMailer.deliveries.length.should == 0 end - + it "setting a new password should fail if new one doesnt validate" do @user.set_password("foobar", "a", "a") @user.errors.any?.should be_true @@ -254,15 +253,15 @@ describe User do @user.errors[:password].length.should == 1 UserMailer.deliveries.length.should == 0 end - + end - + describe "reset_password" do before do @user.confirm_email! @user.save end - + it "fails if the provided email address is unrecognized" do expect { User.reset_password("invalidemail@invalid.com", RESET_PASSWORD_URL) }.to raise_error(JamRuby::JamArgumentError) end @@ -331,7 +330,7 @@ describe User do describe "authenticate (class-instance)" do before { @user.email_confirmed=true; @user.save } - + describe "with valid password" do it { should == User.authenticate(@user.email, @user.password) } end @@ -354,7 +353,7 @@ describe User do end describe "create_dev_user" do - before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } + before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } subject { @dev_user } @@ -365,7 +364,7 @@ describe User do describe "should not be a new record" do it { should be_persisted } end - + describe "updates record" do before { @dev_user = User.create_dev_user("Seth", "Call2", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } @@ -470,22 +469,22 @@ describe User do describe "finalize email updates recurly" do before do - + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") UserMailer.deliveries.clear billing_info = { - first_name: @user.first_name, - last_name: @user.last_name, - address1: 'Test Address 1', - address2: 'Test Address 2', - city: @user.city, - state: @user.state, - country: @user.country, - zip: '12345', - number: '4111-1111-1111-1111', - month: '08', - year: '2017', - verification_value: '111' + first_name: @user.first_name, + last_name: @user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: @user.city, + state: @user.state, + country: @user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' } @recurly.find_or_create_account(@user, billing_info) end @@ -495,30 +494,30 @@ describe User do @recurly.get_account(@user).email.should_not == "somenewemail@blah.com" @finalized = User.finalize_update_email(@user.update_email_token) @recurly.get_account(@user).email.should == "somenewemail@blah.com" - end + end end describe "user_authorizations" do it "can create" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! end it "fails on duplicate" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! @user2 = FactoryGirl.create(:user) - @user2.user_authorizations.build provider: 'facebook', + @user2.user_authorizations.build provider: 'facebook', uid: '1', token: '1', token_expiration: Time.now, @@ -532,7 +531,7 @@ describe User do describe "mods" do it "should allow update of JSON" do - @user.mods = {no_show: {something:1}}.to_json + @user.mods = {no_show: {something: 1}}.to_json @user.save! end end @@ -623,7 +622,7 @@ describe User do end it "remains null if the user's last_jam_addr is null" do - @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start + @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start User.update_locidispids(false) @user.reload @user.last_jam_addr.should be_nil @@ -700,7 +699,7 @@ describe User do end describe "age" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "should calculate age based on birth_date" do user.birth_date = Time.now - 10.years @@ -715,7 +714,7 @@ describe User do end describe "mods_merge" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "allow empty merge" do user.mod_merge({}) @@ -726,7 +725,7 @@ describe User do it "allow no_show set" do user.mod_merge({"no_show" => {"some_screen" => true}}) user.valid?.should be_true - user.mods.should == {no_show:{some_screen:true}}.to_json + user.mods.should == {no_show: {some_screen: true}}.to_json end it "allow no_show aggregation" do @@ -744,22 +743,105 @@ describe User do user.reload user.mod_merge({"no_show" => {"some_screen1" => false}}) user.valid?.should be_true - user.mods.should == {no_show:{some_screen1:false}}.to_json + user.mods.should == {no_show: {some_screen1: false}}.to_json end it "does not allow random root keys" do - user.mod_merge({random_root_key:true}) + user.mod_merge({random_root_key: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_UNKNOWN_KEY] end it "does not allow non-hash no_show" do - user.mod_merge({no_show:true}) + user.mod_merge({no_show: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_MUST_BE_HASH] end end + describe "sync_stripe_customer" do + let(:user) { FactoryGirl.create(:user) } + let(:token1) { create_stripe_token } + let(:token2) { create_stripe_token(2018) } + + # possible Stripe::InvalidRequestError + it "reuses user on card update" do + user.stripe_customer_id.should be_nil + user.payment_update({stripe_token: token1}) + user.reload + user.stripe_customer_id.should_not be_nil + customer1 = user.stripe_customer_id + + # let's change email address too + user.email = 'unique+1@jamkazam.com' + user.save! + + token2.should_not eql token1 + user.payment_update({stripe_token: token2}) + user.reload + user.stripe_customer_id.should_not be_nil + customer2 = user.stripe_customer_id + + customer1.should eql customer2 + # double-check that the stripe customer db record got it's email synced + customer = user.fetch_stripe_customer + customer.email.should eql 'unique+1@jamkazam.com' + + end + end + + describe "can_buy_test_drive?" do + let(:user) { FactoryGirl.create(:user) } + after { + Timecop.return + } + it "works" do + user.can_buy_test_drive?.should be true + FactoryGirl.create(:test_drive_purchase, user: user) + user.can_buy_test_drive?.should be false + Timecop.freeze(Date.today + 366) + user.can_buy_test_drive?.should be true + end + end + + describe "has_rated_teacher" do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher) } + it "works" do + user.has_rated_teacher(teacher).should eql false + review = Review.create(target:teacher, rating:3, user: user) + review.errors.any?.should be false + user.has_rated_teacher(teacher).should be true + end + end + + describe "recent_test_drive_teachers" do + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + it "works" do + user.recent_test_drive_teachers.count.should eql 0 + + testdrive_lesson(user, teacher) + + user.recent_test_drive_teachers[0].id.should eql teacher.id + + end + end + + describe "update_name" do + + let(:user) {FactoryGirl.create(:user)} + + it "typical 2 bits" do + user.update_name("Seth Call") + user.errors.any?.should be_false + user.reload + user.first_name.should eql 'Seth' + user.last_name.should eql 'Call' + 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 dfc84cef3..464f80697 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -34,10 +34,111 @@ describe "RenderMailers", :slow => true do describe "has sending user" do let(:user2) { FactoryGirl.create(:user) } - let(:friend_request) {FactoryGirl.create(:friend_request, user:user, friend: user2)} + let(:friend_request) { FactoryGirl.create(:friend_request, user: user, friend: user2) } it { @filename="text_message"; UserMailer.text_message(user, user2.id, user2.name, user2.resolved_photo_url, 'Get online!!').deliver } - it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver} + it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver } + end + + describe "student/teacher" do + let(:teacher) { u = FactoryGirl.create(:teacher); u.user } + let(:user) { FactoryGirl.create(:user) } + + it "teacher_lesson_request" do + @filename = "teacher_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_request(lesson_booking).deliver + end + + it "student_lesson_request" do + @filename = "student_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + UserMailer.deliveries.clear + UserMailer.student_lesson_request(lesson_booking).deliver + end + + it "teacher_lesson_accepted" do + @filename = "teacher_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver + end + + it "student_lesson_accepted" do + @filename = "student_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver + end + + it "teacher_scheduled_jamclass_invitation" do + @filename = "teacher_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_scheduled_jamclass_invitation(lesson_session.teacher, "custom message", lesson_session.music_session).deliver + end + + it "student_scheduled_jamclass_invitation" do + @filename = "student_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_scheduled_jamclass_invitation(lesson_session.student, "custom message", lesson_session.music_session).deliver + end + + it "student_test_drive_lesson_completed" do + @filename = "student_test_drive_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_completed(lesson).deliver + end + it "student_test_drive_done" do + @filename = "student_test_drive_lesson_done" + lesson = testdrive_lesson(user, teacher) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_done(lesson).deliver + end + it "teacher_lesson_completed" do + @filename = "teacher_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_completed(lesson).deliver + end + end + end + + describe "InvitedSchool emails" do + before(:each) do + UserMailer.deliveries.clear + end + + after(:each) do + UserMailer.deliveries.length.should == 1 + mail = UserMailer.deliveries[0] + save_emails_to_disk(mail, @filename) + end + + + + it "invite_school_teacher" do + @filename = "invite_school_teacher" + UserMailer.invite_school_teacher(FactoryGirl.create(:school_invitation, as_teacher: true)).deliver + end + + it "invite_school_student" do + @filename = "invite_school_student" + UserMailer.invite_school_student(FactoryGirl.create(:school_invitation, as_teacher: false)).deliver end end @@ -120,11 +221,13 @@ describe "RenderMailers", :slow => true do after(:each) do BatchMailer.deliveries.length.should == 1 - mail = BatchMailer.deliveries[0] + mail = BatchMailer.deliveries[0] save_emails_to_disk(mail, @filename) end - it "daily sessions" do @filename="daily_sessions"; scheduled_batch.deliver_batch end + it "daily sessions" do + @filename="daily_sessions"; scheduled_batch.deliver_batch + end end end @@ -140,7 +243,7 @@ def save_emails_to_disk(mail, filename) email_output_dir = 'tmp/emails' FileUtils.mkdir_p(email_output_dir) unless File.directory?(email_output_dir) filename = "#{filename}.eml" - File.open(File.join(email_output_dir, filename), "w+") {|f| + File.open(File.join(email_output_dir, filename), "w+") { |f| f << mail.encoded } end \ No newline at end of file diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 7a4ab16e6..e89881616 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -5,12 +5,14 @@ require 'simplecov' require 'support/utilities' require 'support/profile' require 'support/maxmind' +require 'support/lesson_session' require 'active_record' require 'jam_db' require 'spec_db' require 'uses_temp_files' require 'resque_spec' require 'resque_failed_job_mailer' +require 'stripe_mock' # to prevent embedded resque code from forking ENV['FORK_PER_JOB'] = 'false' @@ -60,6 +62,8 @@ CarrierWave.configure do |config| config.enable_processing = false end +Stripe.api_key = "sk_test_OkjoIF7FmdjunyNsdVqJD02D" + #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' @@ -95,13 +99,13 @@ end config.before(:suite) do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) + DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) end config.around(:each) do |example| # set no_transaction: true as metadata on your test to use deletion strategy instead if example.metadata[:no_transaction] - DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } + DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } else DatabaseCleaner.strategy = :transaction end diff --git a/ruby/spec/support/lesson_session.rb b/ruby/spec/support/lesson_session.rb new file mode 100644 index 000000000..401dad8f4 --- /dev/null +++ b/ruby/spec/support/lesson_session.rb @@ -0,0 +1,79 @@ + +module StripeMock + class ErrorQueue + def clear + @queue = [] + end + end + + def self.clear_errors + instance.error_queue.clear if instance + client.error_queue.clear if client + end +end + +def testdrive_lesson(user, teacher, slots = nil) + + if slots.nil? + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + end + + if user.stored_credit_card == false + user.stored_credit_card = true + user.save! + end + + + booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.") + #puts "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) + 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 +end + + +def normal_lesson(user, teacher, slots = nil) + + if slots.nil? + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + 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.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 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) + #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 +end \ No newline at end of file diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 193f5ed76..ecd19635b 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -258,6 +258,45 @@ def app_config true end + def minimum_lesson_booking_hrs + 24 + end + + def lesson_stay_time + 10 + end + + def wait_time_window_threshold + 5 + end + + def lesson_together_threshold_minutes + 5 + end + def lesson_join_time_window_minutes + 5 + end + def lesson_wait_time_window_minutes + 10 + end + def wait_time_window_pct + 0.8 + end + def end_of_wait_window_forgiveness_minutes + 1 + end + + def test_drive_wait_period_year + 1 + end + + def stripe + { + :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', + :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', + :source_customer => 'cus_88Vp44SLnBWMXq' # seth@jamkazam.com in JamKazam-test account + } + end private def audiomixer_workspace_path @@ -270,7 +309,6 @@ def app_config dev_path = "#{dev_path}/audiomixer/audiomixer/audiomixerapp" dev_path if File.exist? dev_path end - end klass.new @@ -328,3 +366,54 @@ def friend(user1, user2) FactoryGirl.create(:friendship, user: user1, friend: user2) FactoryGirl.create(:friendship, user: user2, friend: user1) end + +def stripe_oauth_client + # from here in the JamKazam Test account: https://dashboard.stripe.com/account/applications/settings + client_id = "ca_88T6HlHg1NRyKFossgWIz1Tf431Jshft" + options = { + :site => 'https://connect.stripe.com', + :authorize_url => '/oauth/authorize', + :token_url => '/oauth/token' + } + + @stripe_oauth_client ||= OAuth2::Client.new(client_id, APP_CONFIG.stripe[:publishable_key], options) +end + +def stripe_account2_id + # seth+stripe+test1@jamkazam.com / jam12345 + "acct_17sCNpDcwjPgpqRL" +end + def stripe_account1_id + # seth+stripe1@jamkazam.com / jam123 +=begin + curl -X POST https://connect.stripe.com/oauth/token \ +-d client_secret=sk_test_OkjoIF7FmdjunyNsdVqJD02D \ +-d code=ac_88U1TDwBgao4I3uYyyFEO3pVbbEed6tm \ +-d grant_type=authorization_code + +# { + "access_token": "sk_test_q8WZbdQXt7RGRkBR0fhgohG6", + "livemode": false, + "refresh_token": "rt_88U3csV42HtY1P1Cd9KU2GCez3wixgsHtIHaQbeeu1dXVWo9", + "token_type": "bearer", + "stripe_publishable_key": "pk_test_s1YZDczylyRUvhAGeVhxqznp", + "stripe_user_id": "acct_17sCNpDcwjPgpqRL", + "scope": "read_write" +} +=end + + + "acct_17sCEyH8FcKpNSnR" + +end + +def create_stripe_token(exp_month = 2017) + Stripe::Token.create( + :card => { + :number => "4111111111111111", + :exp_month => 2, + :exp_year => exp_month, + :cvc => "314" + }, + ).id +end diff --git a/web/Gemfile b/web/Gemfile index e4e4295d9..d21211101 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -46,6 +46,7 @@ gem 'omniauth' gem 'omniauth-facebook' gem 'omniauth-twitter' gem 'omniauth-google-oauth2' +gem 'omniauth-stripe-connect' gem 'google-api-client' #, '0.7.1' #gem 'google-api-omniauth' #, '0.1.1' gem 'signet', '0.5.0' @@ -59,7 +60,6 @@ gem 'carmen' gem 'carrierwave', '0.9.0' gem 'carrierwave_direct' gem 'fog' -gem 'jquery-payment-rails' gem 'haml-rails' gem 'unf' #optional fog dependency gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) @@ -89,12 +89,15 @@ gem 'htmlentities' gem 'sanitize' gem 'recurly' #gem 'guard', '2.7.3' -gem 'influxdb', '0.1.8' -gem 'influxdb-rails', '0.1.10' +gem 'influxdb' #, '0.1.8' +gem 'influxdb-rails'# , '0.1.10' gem 'sitemap_generator' gem 'bower-rails', "~> 0.9.2" gem 'react-rails', '~> 1.0' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' #gem "browserify-rails", "~> 0.7" source 'https://rails-assets.org' do diff --git a/web/app/assets/images/content/stripe-connect-blue-on-dark.png b/web/app/assets/images/content/stripe-connect-blue-on-dark.png new file mode 100644 index 000000000..aec105611 Binary files /dev/null and b/web/app/assets/images/content/stripe-connect-blue-on-dark.png differ diff --git a/web/app/assets/images/content/stripe-connect-light-on-dark.png b/web/app/assets/images/content/stripe-connect-light-on-dark.png new file mode 100644 index 000000000..f9d3433b3 Binary files /dev/null and b/web/app/assets/images/content/stripe-connect-light-on-dark.png differ diff --git a/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png b/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png new file mode 100644 index 000000000..27886fc13 Binary files /dev/null and b/web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png differ diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 16663d4d2..f75d62be4 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -51,6 +51,9 @@ SCHEDULED_SESSION_REMINDER : "SCHEDULED_SESSION_REMINDER", SCHEDULED_SESSION_COMMENT : "SCHEDULED_SESSION_COMMENT", + SCHEDULED_JAMCLASS_INVITATION : "SCHEDULED_JAMCLASS_INVITATION", + LESSON_MESSAGE : "LESSON_MESSAGE", + // recording notifications MUSICIAN_RECORDING_SAVED : "MUSICIAN_RECORDING_SAVED", BAND_RECORDING_SAVED : "BAND_RECORDING_SAVED", diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index aa14917cc..6f144f06a 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -79,6 +79,7 @@ is_affiliate_partner: userDetail.is_affiliate_partner, affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), affiliate_referral_count: userDetail.affiliate_referral_count, + owns_school: !!userDetail.owned_school_id, webcamName: webcamName } , { variable: 'data' })); @@ -143,6 +144,7 @@ $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } ); $("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } ); + $("#account-content-scroller").on('click', '#account-school-link', function(evt) {evt.stopPropagation(); navToSchool(); return false; } ); } function renderAccount() { @@ -200,6 +202,11 @@ window.location = '/client#/account/affiliatePartner' } + function navToSchool() { + resetForm() + window.location = '/client#/account/school' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); diff --git a/web/app/assets/javascripts/accounts_profile_avatar.js b/web/app/assets/javascripts/accounts_profile_avatar.js index 2ab8858cd..de47e1228 100644 --- a/web/app/assets/javascripts/accounts_profile_avatar.js +++ b/web/app/assets/javascripts/accounts_profile_avatar.js @@ -126,7 +126,7 @@ maxSize: 10000*1024, policy: filepickerPolicy.policy, signature: filepickerPolicy.signature - }, { path: createStorePath(self.userDetail), access: 'public' }, + }, { path: createStorePath(self.userDetail), access: 'public' }, function(fpfiles) { removeAvatarSpinner(); afterImageUpload(fpfiles[0]); diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 019ddfcda..6b8e803af 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -39,6 +39,7 @@ //= require jquery.payment //= require jquery.visible //= require jquery.jstarbox +//= require jquery.inputmask //= require fingerprint2.min //= require ResizeSensor //= require classnames diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 8121ad28f..927f8b9b0 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -16,6 +16,9 @@ var $noShow = null; var $noShowCheckbox = null; var $buttons = null; + var $alertIcon = null; + var $noticeIcon = null; + // you can also do // * showAlert('title', 'text') @@ -46,6 +49,20 @@ return show(options); } + function showNotice(options) { + if (typeof options == 'string' || options instanceof String) { + if(arguments.length == 2) { + options = {title: options, html:arguments[1]} + } + else { + options = {html:options}; + } + } + options.type = 'notice' + return show(options); + } + + // responsible for updating the contents of the update dialog // as well as registering for any event handlers function show(options) { @@ -59,6 +76,9 @@ else if(options.type == 'yes_no') { options.title = 'please confirm'; } + else if(options.type == 'notice') { + options.title = 'notice'; + } } hide(); @@ -79,9 +99,17 @@ throw "unable to show banner for empty message"; } + if(options.type == "notice") { + $alertIcon.hide() + $noticeIcon.show() + } + else { + $alertIcon.show() + $noticeIcon.hide() + } - if((options.type == "alert" && !options.buttons) || options.close || options.no_show) { + if(((options.type == "alert" || options.type == "notice") && !options.buttons) || options.close || options.no_show) { var closeButtonText = 'CLOSE'; if(options.close !== null && typeof options.close == 'object') { @@ -183,6 +211,8 @@ $noShowCheckbox = $banner.find('.no-more-show-checkbox') $noShow = $banner.find('.no-more-show') $buttons = $banner.find('.buttons') + $alertIcon = $banner.find('.content-icon.alert') + $noticeIcon = $banner.find('.content-icon.notice') context.JK.checkbox($noShowCheckbox); return self; @@ -194,6 +224,7 @@ show: show, showAlert: showAlert, showYesNo: showYesNo,// shows Yes and Cancel button (confirmation dialog) + showNotice: showNotice, hide: hide } diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index b45cf5325..c9ae84cd4 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2077,6 +2077,16 @@ }); } + function createRedirectHint(data) { + return $.ajax({ + type: "POST", + url: '/api/redirect_hints', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + function signup(data) { return $.ajax({ type: "POST", @@ -2108,6 +2118,82 @@ }) } + function bookLesson(data) { + return $.ajax({ + type: "POST", + url: '/api/lesson_bookings', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data) + }) + } + + + function getLessonBooking(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: '/api/lesson_bookings/' + options.id, + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + + function getUnprocessedLesson(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: '/api/lesson_bookings/unprocessed', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getUnprocessedLessonOrIntent(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: '/api/lesson_bookings/unprocessed_or_intent', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function acceptLessonBooking(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_bookings/' + options.id + '/accept', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function counterLessonBooking(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_bookings/' + options.id + '/counter', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + + function cancelLessonBooking(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_bookings/' + options.id + '/cancel', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + function createAlert(subject, data) { var message = {subject:subject}; $.extend(message, data); @@ -2121,6 +2207,201 @@ }); } + function submitStripe(options) { + return $.ajax({ + type: "POST", + url: '/api/stripe', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getLessonSessions(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions?" + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + + function getTestDriveStatus(options) { + return $.ajax({ + type: "GET", + url: "/api/users/" + options.id + "/test_drive/" + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function createTeacherIntent(options) { + return $.ajax({ + type: "POST", + url: '/api/teachers/' + options.id + "/intent", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getSchool(options) { + + var id = getId(options); + return $.ajax({ + type: "GET", + url: "/api/schools/" + id, + dataType: "json", + contentType: 'application/json' + }); + } + + function updateSchool(options) { + var id = getId(options); + return $.ajax({ + type: "POST", + url: '/api/schools/' + id, + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + + + function updateSchoolAvatar(options) { + var id = getId(options); + + var original_fpfile = options['original_fpfile']; + var cropped_fpfile = options['cropped_fpfile']; + var cropped_large_fpfile = options['cropped_large_fpfile']; + var crop_selection = options['crop_selection']; + + logger.debug(JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + })); + + var url = "/api/schools/" + id + "/avatar"; + return $.ajax({ + type: "POST", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + }) + }); + } + + function deleteSchoolAvatar(options) { + var id = getId(options); + + var url = "/api/schools/" + id + "/avatar"; + return $.ajax({ + type: "DELETE", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false + }); + } + + function generateSchoolFilePickerPolicy(options) { + var id = getId(options); + var handle = options && options["handle"]; + var convert = options && options["convert"] + + var url = "/api/schools/" + id + "/filepicker_policy"; + + return $.ajax(url, { + data : { handle : handle, convert: convert }, + dataType : 'json' + }); + } + + function listSchoolInvitations(options) { + + var id = getId(options); + + return $.ajax({ + type: "GET", + url: "/api/schools/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json' + }); + } + + function createSchoolInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/schools/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteSchoolInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/schools/" + id + '/invitations', + dataType: "json", + contentType: 'application/json' + }); + } + + function resendSchoolInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/schools/" + id + '/resend', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteSchoolStudent(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/schools/" + id + '/students/' + options.user_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function deleteSchoolTeacher(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/schools/" + id + '/teachers/' + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function initialize() { return self; } @@ -2308,10 +2589,34 @@ this.postBandSearchFilter = postBandSearchFilter; this.playJamTrack = playJamTrack; this.createSignupHint = createSignupHint; + this.createRedirectHint = createRedirectHint; this.createAlert = createAlert; this.redeemGiftCard = redeemGiftCard; this.signup = signup; this.portOverCarts = portOverCarts; + this.bookLesson = bookLesson; + this.getLessonBooking = getLessonBooking; + this.getUnprocessedLesson = getUnprocessedLesson; + this.getUnprocessedLessonOrIntent = getUnprocessedLessonOrIntent; + this.acceptLessonBooking = acceptLessonBooking; + this.cancelLessonBooking = cancelLessonBooking; + this.counterLessonBooking = counterLessonBooking; + this.submitStripe = submitStripe; + this.getLessonSessions = getLessonSessions; + this.getTestDriveStatus = getTestDriveStatus; + this.createTeacherIntent = createTeacherIntent; + this.getSchool = getSchool; + this.updateSchool = updateSchool; + this.updateSchoolAvatar = updateSchoolAvatar; + this.deleteSchoolAvatar = deleteSchoolAvatar; + this.generateSchoolFilePickerPolicy = generateSchoolFilePickerPolicy; + this.listSchoolInvitations = listSchoolInvitations; + this.createSchoolInvitation = createSchoolInvitation; + this.deleteSchoolInvitation = deleteSchoolInvitation; + this.resendSchoolInvitation = resendSchoolInvitation; + this.deleteSchoolTeacher = deleteSchoolTeacher; + this.deleteSchoolStudent = deleteSchoolStudent; + return this; }; })(window,jQuery); diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index bb3929c4e..b13b42287 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -124,6 +124,12 @@ */ function ajaxError(jqXHR, textStatus, errorMessage) { + if (!textStatus) { + textStatus = 'n/a' + } + if (!errorMessage) { + errorMessage = 'n/a' + } //var err; //try { throw new Error('lurp'); } catch(e) { err = e }; //console.log("TRACE", JSON.stringify(err)) diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index c552750a3..85a435521 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -506,6 +506,12 @@ $.btOffAll(); // add any prod bubbles if you close a dialog } + function screenProperty(screen, property) { + if (screen && screen in screenBindings) { + return screenBindings[screen][property] + } + return null; + } function screenEvent(screen, evtName, data) { if (screen && screen in screenBindings) { if (evtName in screenBindings[screen]) { @@ -563,8 +569,7 @@ // reset the hash to where it just was context.location.hash = currentHash; } - else { - // not rejected by the screen; let it go + else {(screen) return postFunction(e); } } @@ -583,8 +588,9 @@ logger.debug("layout: changing screen to " + currentScreen); + // notify everyone $(document).triggerHandler(EVENTS.SCREEN_CHANGED, {previousScreen: previousScreen, newScreen: currentScreen}) - + window.NavActions.screenChanged(currentScreen, screenProperty(currentScreen, 'navName')) context.JamTrackPreviewActions.screenChange() screenEvent(currentScreen, 'beforeShow', data); @@ -731,6 +737,13 @@ } logger.debug("opening dialog: " + dialog) + var $dialog = $('[layout-id="' + dialog + '"]'); + if($dialog.length == 0) { + logger.debug("unknown dialog encountered: " + dialog) + return + } + + var $overlay = $('.dialog-overlay') if (opts.sizeOverlayToContent) { @@ -743,12 +756,12 @@ $overlay.show(); centerDialog(dialog); - var $dialog = $('[layout-id="' + dialog + '"]'); stackDialogs($dialog, $overlay); addScreenContextToDialog($dialog) $dialog.show(); // maintain center (un-attach previous sensor if applicable, then re-add always) + window.ResizeSensor.detach($dialog.get(0)) new window.ResizeSensor($dialog, function(){ centerDialog(dialog); diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index c6e1f25bc..fcf3b4474 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -183,6 +183,8 @@ registerScheduledSessionReminder(); registerScheduledSessionComment(); + // jamclass invitation + registerScheduledJamclassInvitation(); // recording notifications registerMusicianRecordingSaved(); @@ -438,6 +440,12 @@ else if (type === context.JK.MessageType.SCHEDULED_SESSION_INVITATION) { linkSessionInfoNotification(payload, $notification, $btnNotificationAction); } + else if (type === context.JK.MessageType.LESSON_MESSAGE) { + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + } + else if (type === context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION) { + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + } else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP) { var $action_btn = $notification.find($btnNotificationAction); $action_btn.text('MANAGE RSVP'); @@ -478,6 +486,14 @@ }); } + function linkLessonInfoNotification(payload, $notification, $btnNotificationAction) { + var $action_btn = $notification.find($btnNotificationAction); + $action_btn.text('LESSON DETAILS'); + $action_btn.click(function() { + gotoLessonBookingPage({"lesson_session_id": payload.lesson_session_id}); + }); + } + function acceptBandInvitation(args) { rest.updateBandInvitation( args.band_id, @@ -906,6 +922,29 @@ }); } + function registerScheduledJamclassInvitation() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION, function(header, payload) { + logger.debug("Handling SCHEDULED_JAMCLASS_INVITATION msg " + JSON.stringify(payload)); + + handleNotification(payload, header.type); + + app.notify({ + "title": "JamClass Scheduled", + "text": payload.msg + "

" + payload.session_name + "
" + payload.session_date, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, [{ + id: "btn-more-info", + text: "MORE INFO", + rel: "external", + "class": "button-orange", + callback: gotoLessonBookingPage, + callback_args: { + "lesson_session_id": payload.lesson_session_id + } + }] + ); + }); + } function registerScheduledSessionRsvp() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP, function(header, payload) { @@ -1337,6 +1376,10 @@ context.JK.popExternalLink('/sessions/' + args.session_id + '/details'); } + function gotoLessonBookingPage(args) { + window.location.href = "/client#/jamclass/lesson-booking/" + 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 aa06843e3..7d83a8e56 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -3,8 +3,12 @@ //= require_directory ./react-components/helpers //= require_directory ./react-components/actions //= require ./react-components/stores/AppStore +//= require ./react-components/stores/NavStore //= require ./react-components/stores/UserStore //= require ./react-components/stores/UserActivityStore +//= require ./react-components/stores/SchoolStore +//= require ./react-components/stores/StripeStore +//= require ./react-components/stores/AvatarStore //= require ./react-components/stores/InstrumentStore //= require ./react-components/stores/LanguageStore //= require ./react-components/stores/GenreStore diff --git a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee new file mode 100644 index 000000000..c91bd6389 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee @@ -0,0 +1,451 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +SchoolActions = context.SchoolActions +SchoolStore = context.SchoolStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountSchoolScreen = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(SchoolStore, "onSchoolChanged") + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + TILE_ACCOUNT: 'account' + TILE_MEMBERS: 'members' + TILE_EARNINGS: 'earnings' + TILE_AGREEMENT: 'agreement' + + TILES: ['account', 'members', 'earnings', 'agreement'] + + onAppInit: (@app) -> + @app.bindScreen('account/school', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onSchoolChanged: (schoolState) -> + @setState(schoolState) + + onUserChanged: (userState) -> + @noSchoolCheck(userState?.user) + @setState({user: userState?.user}) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}] + @root = $(@getDOMNode()) + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({userSchedulingComm: value}) + + + beforeHide: (e) -> + #ProfileActions.viewTeacherProfileDone() + @screenVisible = false + + beforeShow: (e) -> + + noSchoolCheck: (user) -> + if user?.id? && @screenVisible + + if !user.owned_school_id? + window.JK.Banner.showAlert("You are not the owner of a school in our systems. If you are, please contact support@jamkazam.com and we'll update your account.") + return false + else + if !@shownOnce + @shownOnce = true + SchoolActions.refresh(user.owned_school_id) + + return true + + else + return false + + afterShow: (e) -> + @screenVisible = true + logger.debug("AccountSchoolScreen: afterShow") + logger.debug("after show", @state.user) + @noSchoolCheck(@state.user) + + getInitialState: () -> + { + school: null, + user: null, + selected: 'account', + userSchedulingComm: null, + updateErrors: null, + schoolName: null, + studentInvitations: null, + teacherInvitations: null, + updating: false + } + + isSchoolManaged: () -> + if this.state.userSchedulingComm? + this.state.userSchedulingComm == 'school' + else + this.state.school.scheduling_communication == 'school' + + nameValue: () -> + if this.state.schoolName? + this.state.schoolName + else + this.state.school.name + + nameChanged: (e) -> + $target = $(e.target) + val = $target.val() + @setState({schoolName: val}) + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + 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}) + + + if !handled + @app.ajaxError(jqXHR, null, null) + + inviteTeacher: () -> + @app.layout.showDialog('invite-school-user', {d1: true}) + + inviteStudent: () -> + @app.layout.showDialog('invite-school-user', {d1: false}) + resendInvitation: (id, e) -> + e.preventDefault() + rest.deleteSchoolInvitation({ + id: + id: this.state.school.id, invitation_id: id + }).done((response) => @resendInvitationDone(response)).fail((jqXHR) => @resendInvitationFail(jqXHR)) + + resendInvitationDone: (response) -> + @app.layout.notify({title: 'invitation resent', text: 'invitation was resent to ' + response.email}) + + resendInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + deleteInvitation: (id, e) -> + e.preventDefault() + rest.deleteSchoolInvitation({ + id: + id: this.state.school.id, invitation_id: id + }).done((response) => @deleteInvitationDone(id, response)).fail((jqXHR) => @deleteInvitationFail(jqXHR)) + + deleteInvitationDone: (id, response) -> + context.SchoolActions.deleteInvitation(id) + + deleteInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + + removeFromSchool: (id, isTeacher, e) -> + if isTeacher + rest.deleteSchoolTeacher({id: this.state.school.id, teacher_id: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + else + rest.deleteSchoolStudent({id: this.state.school.id, student: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + + removeFromSchoolDone: (school) -> + context.JK.Banner.showNotice("User removed", "User was removed from your school.") + context.SchoolActions.updateSchool(school) + + removeFromSchoolFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + renderUser: (user, isTeacher) -> + photo_url = user.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + `
+
+ +
+
+ {user.name} +
+
+ remove from school +
+
` + + + renderInvitation: (invitation) -> + `
+ + + + + +
{invitation.first_name} {invitation.last_name} +
has not yet accepted invitation
+ resend invitation + delete +
+ +
+
` + + renderTeachers: () -> + teachers = [] + + if this.state.school.teachers? && this.state.school.teachers.length > 0 + for teacher in this.state.school.teachers + teachers.push(@renderUser(teacher.user, true)) + else + teachers = `

No teachers

` + + teachers + + renderStudents: () -> + students = [] + + if this.state.school.students? && this.state.school.students.length > 0 + for student in this.state.school.students + students.push(@renderUser(student, false)) + else + students = `

No students

` + + students + + renderTeacherInvitations: () -> + invitations = [] + + if this.state.teacherInvitations? && this.state.teacherInvitations.length > 0 + for invitation in this.state.teacherInvitations + invitations.push(@renderInvitation(invitation)) + else + invitations = `

No pending invitations

` + invitations + + renderStudentInvitations: () -> + invitations = [] + + if this.state.studentInvitations? && this.state.studentInvitations.length > 0 + for invitation in this.state.studentInvitations + invitations.push(@renderInvitation(invitation)) + else + invitations = `

No pending invitations

` + invitations + + mainContent: () -> + if !@state.user? || !@state.school? + `
Loading...
` + else if @state.selected == @TILE_ACCOUNT + @account() + else if @state.selected == @TILE_MEMBERS + @members() + else if @state.selected == @TILE_EARNINGS + @earnings() + else if @state.selected == @TILE_AGREEMENT + @agreement() + else + @account() + + account: () -> + ownerEmail = this.state.school.owner.email + correspondenceEmail = this.state.school.correspondence_email + correspondenceDisabled = !@isSchoolManaged() + + nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) + correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors) + nameClasses = classNames({name: true, error: nameErrors?, field: true}) + correspondenceEmailClasses = classNames({ + correspondence_email: true, + error: correspondenceEmailErrors?, + field: true + }) + + cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } + updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + + `
+
+ + + {nameErrors} +
+
+ + +
+ +

Management Preference

+ +
+
+ +
+
+ +
+
+
+ + + +
All emails relating to lesson scheduling will go to this email if school owner manages + scheduling. +
+ {correspondenceEmailErrors} +
+ +

Payments

+ +
+ +
+ +
+ CANCEL + UPDATE +
+
` + + + members: () -> + teachers = @renderTeachers() + teacherInvitations = @renderTeacherInvitations() + + students = @renderStudents() + studentInvitations = @renderStudentInvitations() + + `
+
+
+

teachers:

+ INVITE TEACHER +
+
+
+ {teacherInvitations} +
+ +
+ {teachers} +
+
+ +
+
+

students:

+ INVITE STUDENT +
+
+
+ {studentInvitations} +
+
+ {students} +
+
+ +
` + + earnings: () -> + `
+

Coming soon

+
` + + agreement: () -> + `
+

The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the + complete terms of service here. And you can find the section that is + most specific to the music school terms here.

+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @setState({selected: selection}) + + createTileLink: (i, tile) -> + active = this.state.selected == tile + classes = classNames({last: i == @TILES.length - 1, activeTile: active}) + + return `
{tile}
` + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @TILES + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNav = `
+ {profileSelections} +
` + + `
+
+
school:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee new file mode 100644 index 000000000..4b08de848 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee @@ -0,0 +1,55 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AvatarStore = context.AvatarStore + +@AvatarEditLink = React.createClass({ + + mixins: [ + Reflux.listenTo(AvatarStore, "onAvatarUpdate"), + ] + + onAvatarUpdate: (avatar) -> + @setState({avatar: avatar}) + + getInitialState: () -> + { + avatar: null, + imageLoadedFpfile: null + } + + componentWillMount: () -> + + + componentDidMount: () -> + @root = $(@getDOMNode()) + + startUpdate: () -> + AvatarActions.start(this.props.target, this.props.target_type) + + render: () -> + if this.props.target?.photo_url? + + testStudentUrl = "/school/#{this.props.target.id}/student?preview=true" + testTeacherUrl = "/school/#{this.props.target.id}/teacher?preview=true" + + `
+ +
+ change/update logo
+ +
See how it will look to  + students and  + teachers +
+
` + else + `
+
+ No logo graphic uploaded +
+ change/update logo +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee b/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee new file mode 100644 index 000000000..c6e1203f6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee @@ -0,0 +1,168 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AvatarStore = context.AvatarStore + +@AvatarUploader = React.createClass({ + + mixins: [ + Reflux.listenTo(@AppStore, "onAppInit"), + Reflux.listenTo(AvatarStore, "onAvatarUpdate"), + ] + + onAppInit: (@app) -> + + onAvatarUpdate: (avatar) -> + @setState({avatar: avatar}) + + getInitialState: () -> + { + avatar: null, + imageLoadedFpfile: null + } + + componentWillMount: () -> + + + componentDidMount: () -> + @root = $(@getDOMNode()) + + # get filepicker loaded! + `(function (a) { + if (window.filepicker) { + return + } + var b = a.createElement("script"); + b.type = "text/javascript"; + b.async = !0; + b.src = ("https:" === a.location.protocol ? "https:" : "http:") + "//api.filepicker.io/v1/filepicker.js"; + var c = a.getElementsByTagName("script")[0]; + c.parentNode.insertBefore(b, c); + var d = {}; + d._queue = []; + var e = "pick,pickMultiple,pickAndStore,read,write,writeUrl,export,convert,store,storeUrl,remove,stat,setKey,constructWidget,makeDropPane".split(","); + var f = function (a, b) { + return function () { + b.push([a, arguments]) + } + }; + for (var g = 0; g < e.length; g++) { + d[e[g]] = f(e[g], d._queue) + } + window.filepicker = d + })(document);` + + componentDidUpdate: () -> + #$jcropHolder = @root.find('.jcrop-holder') + #$spinner = @root.find('.spinner-large') + # $spinner.width($jcropHolder.width()); + # $spinner.height($jcropHolder.height()); + + startUpdate: (e) -> + e.preventDefault(); + + AvatarActions.pick() + + onSelect: (selection) -> + AvatarActions.select(selection) + + imageLoaded: (e) -> + avatar = $(e.target) + width = avatar.naturalWidth(); + height = avatar.naturalHeight(); + logger.debug("image loaded", avatar, width, height) + + storedSelection = this.state.avatar?.currentCropSelection + if storedSelection? + left = storedSelection.x + right = storedSelection.x2 + top = storedSelection.y + bottom = storedSelection.y2 + else + if width < height + left = width * .25 + right = width * .75 + top = (height / 2) - (width / 4) + bottom = (height / 2) + (width / 4) + else + top = height * .25 + bottom = height * .75 + left = (width / 2) - (height / 4) + right = (width / 2) + (height / 4) + + container = @root.find('.avatar-space') + + jcrop = avatar.data('Jcrop'); + + if jcrop + logger.debug("jcrop destroy!") + jcrop.destroy() + + @root.find('.jcrop-holder').remove() + + logger.debug("initial selection box: left: #{left}, top: #{top}, right: #{right}, bottom: #{bottom}") + avatar.Jcrop({ + aspectRatio: 1, + boxWidth: container.width() * 1, + boxHeight: container.height() * 1, + setSelect: [left, top, right, bottom], + trueSize: [width, height], + onSelect: this.onSelect + }); + + @setState({imageLoadedFpfile: this.state.avatar.currentFpfile}) + + imageLoadError: () -> + logger.debug("image did not load") + + + doCancel: (e) -> + e.preventDefault() + @app.layout.closeDialog('upload-avatar', true); + + doDelete: (e) -> + e.preventDefault() + AvatarActions.delete() + + doSave: (e) -> + AvatarActions.update() + + render: () -> + if this.state.avatar?.signedCurrentFpfile? + fpfile = this.state.avatar.currentFpfile + signedUrl = this.state.avatar.signedCurrentFpfile + #console.log("filefile info", fpfile, signedUrl, this.state.imageLoadedFpfile) + #spinner = `
` + if fpfile.url != this.state.imageLoadedFpfile?.url || this.state.avatar.updatingAvatar || this.state.avatar.pickerOpen + spinner = `
` + + `
+
+ +
+ {spinner} +
+ upload new logo + CANCEL + DELETE + SAVE & CLOSE + +
+
` + + else + `
+
+
+ No logo graphic uploaded +
+ +
+
+ upload new logo +
+ +
` + +}) \ 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/BookLessonFree.js.jsx.coffee new file mode 100644 index 000000000..72edef0cd --- /dev/null +++ b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee @@ -0,0 +1,524 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@BookLesson = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + onAppInit: (@app) -> + @app.bindScreen('jamclass/book-lesson', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + @setState({user: userState?.user}) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({ recurring: value }) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.lesson-frequency', stateKey: 'lesson-frequency'}] + @root = $(@getDOMNode()) + + @slot1Date = @root.find('.slot-1 .date-picker') + @slot2Date = @root.find('.slot-2 .date-picker') + @slot1Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @slot2Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @iCheckify() + + componentDidUpdate:() -> + @iCheckify() + @slot1Date = @root.find('.slot-1 .date-picker') + @slot2Date = @root.find('.slot-2 .date-picker') + @slot1Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @slot2Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + + + toggleDate: (e) -> + + isNormal: () -> + @state.type == 'normal' + + isTestDrive: () -> + @state.type == 'test-drive' + + parseId:(id) -> + if !id? + {id: null, type: null} + else + bits = id.split('_') + if bits.length == 2 + {id: bits[1], type: bits[0]} + else + {id: null, type: null} + + beforeHide: (e) -> + logger.debug("BookLesson: beforeHide") + @resetErrors() + + beforeShow: (e) -> + logger.debug("BookLesson: beforeShow", e.id) + + afterShow: (e) -> + logger.debug("BookLesson: afterShow", e.id) + + parsed = @parseId(e.id) + + id = parsed.id + + @setState({teacherId: id, type: parsed.type}) + @resetErrors() + rest.getUserDetail({ + id: id, + show_teacher: true + }).done((response) => @userDetailDone(response)).fail(@app.ajaxError) + + userDetailDone: (response) -> + if response.id == @state.teacherId + @setState({teacher: response, isSelf: response.id == context.JK.currentUserId}) + else + logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId) + + getInitialState: () -> + { + user: null, + teacher: null, + teacherId: null, + generalErrors: null, + descriptionErrors: null, + bookedPriceErrors: null, + slot1Errors: null, + slot2Errors: null + updating: false, + recurring: 'single' + } + + jamclassPolicies: (e) -> + e.preventDefault() + context.JK.popExternalLink($(e.target).attr('href')) + + getSlotData: (position) -> + $slot = @root.find('.slot-' + (position + 1)) + picker = $slot.find('.date-picker') + + hour = $slot.find('.hour').val() + minute = $slot.find('.minute').val() + am_pm = $slot.find('.am_pm').val() + + + if hour? and hour != '' + hour = new Number(hour) + if am_pm == 'PM' + hour += 12 + else + hour = null + + if minute? and minute != '' + minute = new Number(minute) + else + minute = null + + if !@isRecurring() + date = picker.datepicker("getDate") + if date? + date = context.JK.formatDateYYYYMMDD(date) + else + day_of_week = $slot.find('.day_of_week').val() + + + {hour: hour, minute: minute, date: date, day_of_week: day_of_week} + + resetErrors: () -> + @setState({generalErrors: null, slot1Errors: null, slot2Errors: null, descriptionErrors: null, bookedPriceErrors: null}) + + isRecurring: () -> + @state.recurring == 'recurring' + + isMonthly: () -> + if !@isRecurring() + return false + + parsed = @bookingOption() + return parsed? && parsed.frequency == 'monthly' + + bookingOption: () -> + select = @root.find('.booking-options-for-teacher') + value = select.val() + @parseBookingOption(value) + + # select format = frequency|lesson_length , where frequency is 'monthly' or 'weekly' + parseBookingOption: (value) -> + if !value? + return null + bits = value.split('|') + if !bits? || bits.length != 2 + return null + return {frequency: bits[0], lesson_length: bits[1]} + + + onBookLesson: (e) -> + e.preventDefault() + + if $(e.target).is('.disabled') + return + + options = {} + options.teacher = this.state.teacher.id + + options.slots = [@getSlotData(0), @getSlotData(1)] + options.timezone = window.jstz.determine().name() + description = @root.find('textarea.user-description').val() + if description == '' + description == null + options.description = description + + if @isTestDrive() + options.payment_style = 'elsewhere' + options.lesson_type = 'test-drive' + + else if @isNormal() + options.lesson_type = 'paid' + if @isRecurring() + if @isMonthly() + options.payment_style = 'monthly' + else + options.payment_style = 'weekly' + else + options.payment_style = 'single' + + options.recurring = @isRecurring() + parsed = @bookingOption() + if parsed? + options.lesson_length = parsed.lesson_length + + + else + throw "Unable to determine lesson type" + + @resetErrors() + @setState({updating: true}) + rest.bookLesson(options).done((response) => @booked(response)).fail((jqXHR) => @failedBooking(jqXHR)) + + booked: (response) -> + @setState({updating: false}) + if response.user['has_stored_credit_card?'] + context.location ="/client#/jamclass/lesson-session/" + response.id + else + context.location = '/client#/jamclass/payment' + + failedBooking: (jqXHR) -> + @setState({updating: false}) + if jqXHR.status == 422 + body = JSON.parse(jqXHR.responseText) + + generalErrors = {errors: {}} + for errorType, errors of body.errors + if errorType == 'description' + @setState({descriptionErrors: errors}) + else if errorType == 'booked_price' + @setState({bookedPriceErrors: errors}) + else if errorType == 'lesson_length' + # swallow, because 'booked_price' covers this + else if errorType == 'lesson_booking_slots' + # do nothing. these are handled better by the _children errors + else + generalErrors.errors[errorType] = errors + + for childErrorType, childErrors of body._children + if childErrorType == 'lesson_booking_slots' + slot1Errors = childErrors[0] + slot2Errors = childErrors[1] + if Object.keys(slot1Errors["errors"]).length > 0 + @setState({slot1Errors: slot1Errors}) + if Object.keys(slot2Errors["errors"]).length > 0 + @setState({slot2Errors: slot2Errors}) + if Object.keys(generalErrors.errors).length > 0 + @setState({generalErrors: generalErrors}) + + onCancel: (e) -> + e.preventDefault() + + isTestDrive: () -> + @state.type == 'test-drive' + + isNormal: () -> + @state.type == 'normal' + + constructBookingOptions: () -> + results = [] + if !@state.teacher? + return results + + teacher = @state.teacher.teacher + enabledMinutes = [] + for minutes in [30, 45, 60, 90, 120] + duration_enabled = teacher["lesson_duration_#{minutes}"] + + if duration_enabled + enabledMinutes.push(minutes) + + if !@isRecurring() + for minutes in enabledMinutes + lesson_price = teacher["price_per_lesson_#{minutes}_cents"] + value = "single|#{minutes}" + display = "#{minutes} Minute Lesson for $#{(lesson_price / 100).toFixed(2)}" + results.push(``) + else + for minutes in enabledMinutes + lesson_price = teacher["price_per_lesson_#{minutes}_cents"] + value = "single|#{minutes}" + display = "#{minutes} Minute Lesson Each Week - $#{(lesson_price / 100).toFixed(2)} Per Week" + results.push(``) + + + for minutes in enabledMinutes + monthly_price = teacher["price_per_month_#{minutes}_cents"] + value = "monthly|#{minutes}" + display = "#{minutes} Minute Lesson Each Week - $#{(monthly_price / 100).toFixed(2)} Per Month" + results.push(``) + + if results.length == 0 + results.push(``) + else + results.unshift(``) + results + + render: () -> + photo_url = teacher?.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + teacher = @state.teacher + + if teacher? + name = `
{teacher.name}
` + teacher_first_name = teacher.first_name + else + name = `
Loading...
` + teacher_first_name = '...' + + hours = [] + for hour in ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] + if hour == '12' + key = '00' + else + key = hour + hours.push(``) + + minutes = [] + for minute in ['00', '15', '30', '45'] + minutes.push(``) + + am_pm = [``, ``] + + bookLessonClasses = classNames({"button-orange": 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) + bookedPriceErrors = context.JK.reactSingleFieldErrors('booked_price', @state.bookedPriceErrors) + slot1Errors = context.JK.reactErrors(@state.slot1Errors, {preferred_day: 'Date', day_of_week: 'Day'}) + slot2Errors = context.JK.reactErrors(@state.slot2Errors, {preferred_day: 'Date', day_of_week: 'Day'}) + generalErrors = context.JK.reactErrors(@state.generalErrors, {user: 'You'}) + + bookedPriceClasses = classNames({booked_price: true, error: bookedPriceErrors?, field: true, 'booking-options': true}) + descriptionClasses = classNames({description: true, error: descriptionErrors?}) + slot1Classes = classNames({slot: true, 'slot-1': true, error: slot1Errors?}) + slot2Classes = classNames({slot: true, 'slot-2': true, error: slot2Errors?}) + generalClasses = classNames({general: true, error: generalErrors?}) + + + if !@isRecurring() + slots = + `
+
+
What date/time do you prefer for your lesson?
+
+ + + +
+
+ + : +
+ {slot1Errors} +
+
+
What is a second date/time option if preferred not available?
+
+ + + +
+
+ + : +
+ {slot2Errors} +
+
` + else + days = [] + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + + + slots = + `
+
+
What day/time do you prefer for your lesson?
+
+ + + +
+
+ + : +
+ {slot1Errors} +
+
+
What is a second day/time option if preferred not available?
+
+ + + +
+
+ + : +
+ {slot2Errors} +
+
` + + + if @isTestDrive() + header = `

book testdrive lesson

` + if @state.user?.remaining_test_drives == 1 + testDriveLessons = "1 TestDrive lesson credit" + else + testDriveLessons = "#{this.state.user?.remaining_test_drives} TestDrive lesson credits" + + actions = `
+ CANCEL + BOOK TESTDRIVE LESSON +
` + + columnLeft = `
+ {header} + {slots} +
+
Tell {teacher_first_name} a little about yourself as a student.
+ +
+ CANCEL + {actionBtnText} +
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee new file mode 100644 index 000000000..b166a9404 --- /dev/null +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -0,0 +1,482 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@LessonPayment = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shouldShowNameSet: false + + onAppInit: (@app) -> + @app.bindScreen('jamclass/lesson-payment', + {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()) + @root.find('input.expiration').payment('formatCardExpiry') + @root.find("input.card-number").payment('formatCardNumber') + @root.find("input.cvv").payment('formatCardCVC') + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + getInitialState: () -> + { + user: null, + lesson: null, + updating: false, + billingInUS: true, + userWantsUpdateCC: false + } + + beforeHide: (e) -> + @resetErrors() + + beforeShow: (e) -> + + afterShow: (e) -> + @resetState() + @resetErrors() + @setState({updating: true}) + rest.getUnprocessedLessonOrIntent().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR)) + + resetErrors: () -> + @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null}) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + @setState({billingInUS: checked}) + + resetState: () -> + @setState({updating: false, lesson: null}) + + unprocessLoaded: (response) -> + @setState({updating: false}) + logger.debug("unprocessed loaded", response) + @setState(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") + + failedUnprocessLoad: (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.' + }) + + onBack: (e) -> + e.preventDefault() + window.location.href = '/client#/teachers/search' + + 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 + if !@state.userWantsUpdateCC + @attemptPurchase(null) + 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, + } + + window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response))); + + stripeResponseHandler: (status, response) -> + console.log("response", response) + + if response.error + 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}) + else + @attemptPurchase(response.id) + + attemptPurchase: (token) -> + if this.state.billingInUS + zip = @root.find('input.zip').val() + + data = { + token: token, + zip: zip, + test_drive: @state.lesson?.lesson_type == 'test-drive' || (@state.intent?.intent == 'book-test-drive') + } + + if @state.shouldShowName + data.name = @root.find('#set-user-on-card').val() + + rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR)) + + stripeSubmitted: (response) -> + logger.debug("stripe submitted", 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?.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 + + 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 + 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 + else + window.location = "/client#/teachers/search" + + stripeSubmitFailure: (jqXHR) -> + 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 Purchase Test Drive", 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?'] + + render: () -> + disabled = @state.updating || @reuseStoredCard() + + if @state.updating + photo_url = '/assets/shared/avatar_generic.png' + name = 'Loading ...' + teacherDetails = `
+
+ +
+ {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 lesson_type == 'single-free' + header = `

enter card info

+ +
Your card wil not be charged.
See explanation to the right.
+
` + 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. +
+ +

jamclass + policies
+

` + else if lesson_type == 'test-drive' + + + if @reuseStoredCard() + header = `

purchase test drive

` + else + header = `

enter payment info for test drive

` + + bookingInfo = `

` + bookingDetail = `

You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles + you to take 4 private online music lessons - 1 each from 4 different instructors in the JamClass instructor + community. +
+ + jamclass + policies +

` + else if lesson_type == 'paid' + 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)}

` + 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. +
+ + jamclass + policies +

` + else if this.state.lesson.payment_style == 'weekly' + bookingInfo = `

You are booking a weekly recurring series of {lesson_length}-minute + lessons, to be paid individually as each lesson is taken, until cancelled.

` + bookingDetail = `

+ Your card will be charged on the day of each lesson. If you need to cancel a lesson, you must do so at + least 24 hours before the lesson is scheduled, or you will be charged for the lesson in full. +
+ + jamclass + policies +

` + else if this.state.lesson.payment_style == 'monthly' + bookingInfo = `

You are booking a weekly recurring series of {lesson_length}-minute + lessons, to be paid for monthly until cancelled.

` + bookingDetail = `

+ Your card will be charged on the first day of each month. Canceling individual lessons does not earn a + refund when buying monthly. To cancel, you must cancel at least 24 hours before the beginning of the + month, or you will be charged for that month in full. +
+ + jamclass + policies +

` + else + bookingInfo = `

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

` + 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. +
+ + jamclass + policies +

` + 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 = ` +

+ 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. +
+ + jamclass policies +

` + + + submitClassNames = {'button-orange': true, disabled: disabled && @state.updating} + updateCardClassNames = {'button-grey': 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.user?['has_stored_credit_card?'] + if @state.userWantsUpdateCC + updateCardAction = `NEVERMIND, USE MY STORED PAYMENT INFO` + leftPurchaseActions = `
+ BACK + {updateCardAction} + PURCHASE +
` + else + updateCardAction = `I'D LIKE TO UPDATE MY PAYMENT INFO` + rightPurchaseActions = `
+ BACK + {updateCardAction} + PURCHASE +
` + else + leftPurchaseActions = `
+ BACKSUBMIT CARD INFORMATION +
` + if @state.shouldShowName && @state.user?.name? + username = @state.user?.name + nameField = + `
+ + +
` + + `
+
+ {header} + +
+ {nameField} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ {leftPurchaseActions} +
+
+ {teacherDetails} +
+ {bookingInfo} + {bookingDetail} + {rightPurchaseActions} +
+
+
+ +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee new file mode 100644 index 000000000..16d63fdef --- /dev/null +++ b/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee @@ -0,0 +1,58 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@LessonSession = React.createClass({ + + mixins: [ + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + onAppInit: (@app) -> + @app.bindScreen('jamclass/lesson-session', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + + @setState({user: userState?.user}) + + componentDidMount: () -> + + @root = $(@getDOMNode()) + + getInitialState: () -> + { + user: null, + lesson: null, + updating: false, + } + + beforeHide: (e) -> + @resetErrors() + + beforeShow: (e) -> + + afterShow: (e) -> + @setState({updating: true}) + + + render: () -> + header = "header" + + `
+
+ {header} + +
+
+ +
+ +
+ +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee b/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee new file mode 100644 index 000000000..f0c9d7730 --- /dev/null +++ b/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee @@ -0,0 +1,34 @@ +context = window +teacherActions = window.JK.Actions.Teacher + +@Nav = React.createClass({ + + mixins: [ + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(NavStore, "onNavChanged") + ] + + onAppInit: (@app) -> + + onNavChanged: (nav) -> + @setState({nav: nav}) + + render: () -> + navs = [] + if this.state?.nav? + nav = this.state.nav + if nav.currentSection? + navs.push(` : `) + navs.push(`{nav.currentSection.name}`) + if nav.optionalParent? + navs.push(` : `) + navs.push(`{nav.optionalParent.name}`) + if nav.currentScreenName? + navs.push(` : `) + navs.push(`{nav.currentScreenName}`) + + `
+ JamKazam Home + {navs} +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee index e6a097dcd..79c17b269 100644 --- a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee @@ -39,18 +39,18 @@ logger = context.JK.logger @props.onItemChanged(country, region) render: () -> - countries = [``] + countries = [``] for countryId, countryInfo of @state.countries - countries.push(``) + countries.push(``) country = @state.countries[@state.selectedCountry] - regions = [``] + regions = [``] if country? && country.regions for region in country.regions - regions.push(``) + regions.push(``) disabled = regions.length == 1 `
diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index c214aab20..5a4450313 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -160,7 +160,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds SessionActions.downloadingJamTrack(false) - console.log("JamTrackPlay: result", ) + console.log("JamTrackPlay: result", result) if !result @app.notify( { diff --git a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee new file mode 100644 index 000000000..b6ef5d193 --- /dev/null +++ b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee @@ -0,0 +1,40 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@StripeConnect = React.createClass({ + + + getInitialState: () -> + { + clicked:false + } + + onStripeConnect: (e) -> + + if this.state.clicked + return + + e.preventDefault() + this.setState({clicked: true}) + StripeActions.connect(this.props.purpose) + + render: () -> + + 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.state.clicked + imageUrl = '/assets/content/stripe-connect-light-on-dark.png' + else + imageUrl = '/assets/content/stripe-connect-blue-on-dark.png' + + `
+ + +
` + + +}) 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 e6f720de5..28b9f2378 100644 --- a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee @@ -60,7 +60,7 @@ logger = context.JK.logger render: () -> endDate = [] if this.props.showEndDate - endDate.push ` + endDate.push ` ` dtLabel = "Start & End" else 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 ca93b873e..45861f89f 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -1,5 +1,4 @@ context = window -MIX_MODES = context.JK.MIX_MODES rest = context.JK.Rest() logger = context.JK.logger @@ -251,7 +250,7 @@ proficiencyDescriptionMap = {

{this.state.user.first_name} Teaches {this.editProfileLink('edit teaching', 'basics')}

- +
{teachesInfo} diff --git a/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee index f1c3e09ce..9a0d77fb1 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee @@ -44,7 +44,8 @@ LocationActions = @LocationActions onSearch: (e) -> e.preventDefault() - TeacherSearchActions.search({searchOptions: @state.options}) + logger.debug("teacher search options", @state.options) + TeacherSearchActions.search(@state.options) levelChanged: (e) -> @@ -67,7 +68,7 @@ LocationActions = @LocationActions selectedAge = null yearsTeaching = [] for yr in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 45, 50] - yearsTeaching.push(``) + yearsTeaching.push(``) `

search teachers

@@ -100,15 +101,15 @@ LocationActions = @LocationActions
- +
- +
- +
diff --git a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee index ab918bd0f..2db743425 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -15,10 +15,16 @@ ProfileActions = @ProfileActions # Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchChanged"), mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged"), - Reflux.listenTo(@TeacherSearchResultsStore, "onTeacherSearchResultsStore")] + Reflux.listenTo(@TeacherSearchResultsStore, "onTeacherSearchResultsStore"), + Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchStore")] LIMIT: 20 visible: false + needToSearch: true + root: null + endOfList: null + contentBodyScroller: null + refreshing: false getInitialState: () -> {searchOptions: {}, results: []} @@ -32,10 +38,25 @@ ProfileActions = @ProfileActions afterShow: (e) -> @visible = false #@setState(TeacherSearchStore.getState()) - TeacherSearchResultsActions.reset() + #if @state.results.length == 0 + # don't issue a new search every time someone comes to the screen, to preserve location from previous browsing + if @needToSearch + @contentBodyScroller.off('scroll') + @endOfList.hide() + + TeacherSearchResultsActions.reset() + @needToSearch = false afterHide: (e) -> + onTeacherSearchStore: (storeChanged) -> + @needToSearch = true + + onTeacherSearchResultsStore: (results) -> + results.searching = false + @refreshing = false + @contentBodyScroller.find('.infinite-scroll-loader-2').remove() + @setState(results) onUserChanged: (@user) -> @@ -46,6 +67,24 @@ ProfileActions = @ProfileActions componentDidMount: () -> @root = $(@getDOMNode()) @resultsNode = @root.find('.results') + @endOfList = @root.find('.end-of-teacher-list') + @contentBodyScroller = @root + + 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 Teachers ...
') + @refreshing = true + @setState({searching: true}) + logger.debug("refreshing more teachers for infinite scroll") + TeacherSearchResultsActions.nextPage() + ) componentDidUpdate: () -> @resultsNode.find('.teacher-bio').each((index, element) => ( @@ -55,17 +94,51 @@ ProfileActions = @ProfileActions after: "a.readmore" }) )) - onTeacherSearchResultsStore: (results) -> - @setState(results) + + + if @state.next == null + @contentBodyScroller.off('scroll') + if @state.currentPage == 1 and @state.results.length == 0 + @endOfList.text('No Teachers found matching your search').show() + logger.debug("TeacherSearch: empty search") + else if @state.currentPage > 0 + logger.debug("end of search") + @endOfList.text('No more Teachers').show() + else + @registerInfiniteScroll(@contentBodyScroller) moreAboutTeacher: (user, e) -> e.preventDefault() ProfileActions.viewTeacherProfile(user, '/client#/teachers/search', 'BACK TO TEACHER SEARCH') - bookTestDrive: (e) -> + bookTestDrive: (user, e) -> e.preventDefault() + rest.getTestDriveStatus({id: context.JK.currentUserId, teacher_id: user.id}) + .done((response) => + if response.remaining_test_drives == 0 && response['can_buy_test_drive?'] + logger.debug("TeacherSearchScreen: user offered test drive") + @app.layout.showDialog('try-test-drive', {d1: user.teacher.id}) + else if response.remaining_test_drives > 0 + if response.booked_with_teacher && !context.JK.currentUserAdmin + logger.debug("TeacherSearchScreen: teacher already test-drived") + + context.JK.Banner.showAlert('TestDrive', "You have already take a TestDrive lesson from this teacher. With TestDrive, you need to use your lessons on 4 different teachers to find one who is best for you. We're sorry, but you cannot take multiple TestDrive lessons from a single teacher.") + else + # send on to booking screen for this teacher + logger.debug("TeacherSearchScreen: user being sent to book a lesson") + + window.location.href = '/client#/jamclass/book-lesson/test-drive_' + user.id + else + # user has no remaining test drives and can't buy any + logger.debug("TeacherSearchScreen: test drive all done") + context.JK.Banner.showAlert('TestDrive', "You have already taken advantage of the TestDrive program within the past year, so we are sorry, but you are not eligible to use TestDrive again now. You may book a normal lesson with this teacher by closing this message, and clicking the BOOK LESSON button for this teacher.") + ) + .fail((jqXHR, textStatus, errorMessage) => + @app.ajaxError(jqXHR, textStatus, errorMessage) + ) + bookNormalLesson: (e) -> e.preventDefault() @@ -82,7 +155,7 @@ ProfileActions = @ProfileActions teacherBio.css('height', 'auto') createSearchDescription: () -> - searchOptions = TeacherSearchStore.getState().searchOptions + searchOptions = TeacherSearchStore.getState() summary = '' instruments = searchOptions.instruments @@ -158,8 +231,7 @@ ProfileActions = @ProfileActions @@ -169,15 +241,17 @@ ProfileActions = @ProfileActions `
- LESSONS HOME :  - TEACHERS SEARCH :  + JamKazam Home :  + JamClass Home :  + Teachers Search :  - SEARCH RESULTS / + Search Results / {searchDesc}
{resultsJsx} +
No more Teachers
` diff --git a/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee new file mode 100644 index 000000000..aff9f15fa --- /dev/null +++ b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee @@ -0,0 +1,99 @@ +context = window +ConfigureTracksStore = @ConfigureTracksStore +ConfigureTracksActions = @ConfigureTracksActions +@TryTestDriveDialog = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + teacher: null + + beforeShow: (args) -> + # d1 should be a teacher ID (vs a user ID that happens to be a teacher) + logger.debug("TryTestDriveDialog.beforeShow", args.d1) + @teacher = args.d1 + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('try-test-drive', dialogBindings); + + getInitialState: () -> + {} + + doCancel: (e) -> + e.preventDefault() + @app.layout.closeDialog('try-test-drive', true); + + doTestDriveNow: (e) -> + e.preventDefault() + + rest.createTeacherIntent({id: @teacher, intent: 'book-test-drive'}).done((response) => @marked(response)).fail((jqXHR) => @failedMark(jqXHR)) + + marked: () -> + @app.layout.closeDialog('try-test-drive') + window.location.href = "/client#/jamclass/lesson-payment" + + failedMark: (jqXHR, textStatus, errorMessage) -> + @app.ajaxError(jqXHR, textStatus, errorMessage) + + render: () -> + `
+
+ + +

TestDrive

+
+
+ +

What Is TestDrive?

+ +

+ Our TestDrive package is the best way to painlessly find the best instructor for you - i.e. a teacher who has + the qualifications you're looking for, and with whom you really connect. This is the most critical factor in + achieving the results you want from your investment of time and money into lessons.

+ +

With TestDrive you pay just $49.99 to take 4 full lessons - one lesson with each of 4 different teachers you + select. Then you can pick the teacher who works best for you. It's an amazing value, highly convenient, and + the best way to find the right teacher for you.

+ +

If you're serious about getting started on lessons, this is the way to go.

+ + +
+
` + + inputChanged: (e) -> + $root = $(@getDOMNode()) + + deletePath: (path, e) -> + e.preventDefault() + + ConfigureTracksActions.removeSearchPath(path) + + selectVSTDirectory: (e) -> + e.preventDefault() + + ConfigureTracksActions.selectVSTDirectory() + + doClose: (e) -> + e.preventDefault() + + @app.layout.closeDialog('try-test-drive', true) + + componentDidMount: () -> + $root = $(@getDOMNode()) + + componentWillUpdate: () -> + @ignoreICheck = true + $root = $(@getDOMNode()) + + componentDidUpdate: () -> + $root = $(@getDOMNode()) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee new file mode 100644 index 000000000..315f3193e --- /dev/null +++ b/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee @@ -0,0 +1,46 @@ +context = window + +@UploadAvatarDialog = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + + + beforeShow: (args) -> + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('upload-avatar', dialogBindings); + + getInitialState: () -> + {} + + render: () -> + `
+
+ + +

Update Logo

+
+
+ + + +
+
` + + + componentDidMount: () -> + $root = $(@getDOMNode()) + + componentWillUpdate: () -> + $root = $(@getDOMNode()) + + componentDidUpdate: () -> + $root = $(@getDOMNode()) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee new file mode 100644 index 000000000..130a2f213 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee @@ -0,0 +1,10 @@ +context = window + +@AvatarActions = Reflux.createActions({ + start: {} + select: {} + pick: {} + update: {} + delete: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee new file mode 100644 index 000000000..5e3828859 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee @@ -0,0 +1,7 @@ +context = window + +@NavActions = Reflux.createActions({ + setScreenInfo: {} + screenChanged: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee new file mode 100644 index 000000000..8977445d2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@SchoolActions = Reflux.createActions({ + refresh: {}, + addInvitation: {}, + deleteInvitation: {} + updateSchool: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee new file mode 100644 index 000000000..33d27d6e1 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee @@ -0,0 +1,6 @@ +context = window + +@StripeActions = Reflux.createActions({ + connect: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee index 924187040..7b39d44e5 100644 --- a/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee @@ -2,5 +2,6 @@ context = window @TeacherSearchResultsActions = Reflux.createActions({ reset: {} + nextPage: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee index daf5708ab..815277def 100644 --- a/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee @@ -3,5 +3,6 @@ context = window @UserActions = Reflux.createActions({ loaded: {} modify: {} + refresh: {} }) diff --git a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee new file mode 100644 index 000000000..204353571 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee @@ -0,0 +1,394 @@ +context = window +rest = context.JK.Rest() + +@JamClassAffiliateLandingBottomPage = React.createClass({ + + render: () -> + `
+
+

What Is The JamKazam Affiliate Program?

+ +

If you’re a music store and you sell products like instruments and accessories to musicians, + + JamKazam can add value to every sale you make to customers by enabling you to bundle high-value digital music products and services that are highly complementary to the products you + + are selling – at no additional cost to your customers. For example, you can bundle full + + multitrack recordings from our catalog of 4,000+ popular songs that let your customers play + + along with their favorite songs in incredibly innovative and fun ways, and you can + bundle free + + online music lessons that really work (unlike Skype, which is great for voice chat but horrible for + + music lessons) to help your customers connect with great teachers and stay better engaged + + with their instruments, so they’ll keep coming back for more music products from your store. + + Additionally, when your customers love these digital products and services and choose to buy + + more, we pay you a share of all revenues we earn from these sales, boosting your revenue and + + profit per sale, while simultaneously delivering greater value to your customer.

+ +

If you’re a music school, JamKazam represents a platform far superior to Skype that you can use + + freely to teach your existing students online, both in your area and across the country to reach + + new markets and students. Skype’s audio quality for music is very poor. Its latency is so high + + that teacher and student cannot play together, and it suffers from a number of other critical + + limitations for music lessons, as it was built for voice chat – not music. In addition to delivering + + online music lessons that really work, JamKazam can drive more students to your school + and + + your teachers from our online lesson marketplace, helping you build your business faster and + + bigger.

+ +

And if you’re both a music store and school, we can help you win in all these ways together. If + + this sounds interesting, read on to learn more about some of the unique things we’ve done and + + how they can help to grow your business.

+ +
+

JamTracks Kudos

+
+ + + +

Andy Crowley of AndyGuitar

+
+
+ + + +

Ryan Jones of PianoKeyz

+
+
+ + + +

Carl Brown of GuitarLessions365

+
+
+
+ +
+

JamTracks – The Best Way To Play With Your Favorite Songs

+ +

+ JamTracks are full multitrack recordings of 4,000+ popular songs that deliver amazing fun and + + flexibility for musicians to play with their favorite artists and tunes. With JamTracks, you can: +

+
    +
  • Listen to just a single isolated part to learn it
  • +
  • Mute the part you want to play, and play along with the rest of the band
  • +
  • Slow down playback to practice without changing the pitch
  • +
  • Change the song key by raising or lowering pitch in half steps
  • +
  • Save custom mixes for easy access, and export them to use anywhere
  • +
  • Make audio and video recordings to share via Facebook, YouTube, etc.
  • +
  • Play online live and in sync with others from different locations
  • +
  • Apply VST & AU audio plugin effects to your live performance
  • +
  • Use MIDI with VST & AU instruments for keys, electronic drums, etc.
  • +
  • And more…
  • +
+

+ JamTracks sell for $1.99 each. Musicians love to play with these, and typically buy a few at a + + time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a + + JamKazam affiliate, you can bundle free JamTracks with the strings sale, delighting your + + customer with the added value. And then when he/she buys more JamTracks, we pay you a + + share of that revenue, adding to your revenues, and improving your margin on your sales. This + + can be applied to just about anything you sell, as we have JamTracks with parts for almost every + + instrument, as well as vocals. And it’s easy to bundle, as you don’t have to specify particular + + songs per product that you sell. You can just give your customer a free JamTracks credit with + + each sale, and your customer can then redeem this credit online to choose their favorite songs. +

+ +

+ Here is a video that shows more about how JamTracks work. +

+
+