diff --git a/db/manifest b/db/manifest index 3719337d9..e229d8d59 100755 --- a/db/manifest +++ b/db/manifest @@ -364,4 +364,5 @@ immediate_recordings.sql nullable_user_id_jamblaster.sql rails4_migration.sql non_free_jamtracks.sql -retailers.sql \ No newline at end of file +retailers.sql +second_ed.sql \ No newline at end of file diff --git a/db/up/second_ed.sql b/db/up/second_ed.sql new file mode 100644 index 000000000..f89ce3014 --- /dev/null +++ b/db/up/second_ed.sql @@ -0,0 +1,4 @@ +ALTER TABLE schools ADD COLUMN education BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE teacher_distributions ADD COLUMN education BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE lesson_bookings ADD COLUMN same_school_free BOOLEAN NOT NULL DEFAULT FALSE; +UPDATE lesson_bookings SET same_school_free = true where same_school = true; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 14b491c2b..dc4c31eac 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -52,6 +52,7 @@ module JamRuby def student_welcome_message(user) @user = user @subject = "Welcome to JamKazam and JamClass online lessons!" + @education = user.school && user.school.education sendgrid_category "Welcome" sendgrid_unique_args :type => "welcome_message" @@ -68,6 +69,9 @@ module JamRuby def teacher_welcome_message(user) @user = user @subject= "Welcome to JamKazam and JamClass online lessons!" + + @education = user.teacher && user.teacher.school && user.teacher.school.education + sendgrid_category "Welcome" sendgrid_unique_args :type => "welcome_message" diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb index d2d9a47f2..10426f858 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -6,6 +6,33 @@

<% end %> +<% if @education %> +

+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was built from the ground up for playing music live in sync with studio quality audio from different locations over the Internet, and for delivering amazing online music lessons. +

+

+ To get ready to take JamClass lessons online, here are the things you'll want to do: +

+ +

1. Set Up Your Gear
+ When you sign up, someone from JamKazam will get in touch with you via email within a couple of business days to help you get set up. If you don't hear from us within a couple of days, please email us at support@jamkazam.com or call us at 1-877-376-8742. To play in online lessons, you will need at a minimum: (1) a Windows or Mac computer; (2) normal home Internet service; and (3) a pair of headphones or earbuds you can plug into the headphone minijack on your computer. If you would like to benefit from studio quality audio (recommended) in your lessons, JamKazam offers an amazing audio package for just $49.99 (less than our cost) that includes an audio interface (a little box that connects to your computer via USB cable), a microphone, a mic cable, and a mic stand. We'll discuss these options with you, and we're happy to support you whichever path you choose. We'll help step you through the setup process, and we'll even get into an online session with you to make sure everything is working properly, and to show you some of the key features you can use during online lessons. + +

+ +

2. Book Lessons
+ Once your gear is set up, you are ready to take lessons. Go to this web page: <%= @user.school.teacher_list_url %>. If your school has preferred instructors, they will be listed on this page, and you can click a button to book a lesson with the teacher from whom you want to take lessons. If your school doesn't have preferred instructors, then there is a link on this page to use our teacher search feature to find a great instructor from our broader community of teachers. You'll need your parents to enter credit card information to pay for your lessons. We use one of the largest and most secure commerce platforms on the Internet called Stripe, so you can feel confident your financial information will be very secure. +

+ +

3. Learn About JamClass Features
+ You can also review our JamClass user guide for students + to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to join your teacher in online lessons, features you can use while in lessons, and more. +

+ +

+ Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician! +

+<% else %> +

Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was @@ -47,6 +74,7 @@ Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician!

+<% end %>

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.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb index 0590bfa1c..98e626d51 100644 --- 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 @@ -30,14 +30,19 @@

2. Set Up Your Gear
- Click - here for information on the gear requirements to effectively teach using the JamClass service. When you have - everything you need, - use - this set of help articles as a good step-by-step guide to set up your gear for use with the - JamKazam application. After you have signed up, someone from JamKazam will contact you to schedule a test online - session, in which we will make sure your audio and video gear are working properly in an online session, and to make - sure you feel comfortable with the key features you will be using in sessions with students. + <% if @education %> + Click + here for information on the gear requirements to effectively teach using the JamClass service. At a minimum, you'll need a Windows or Mac computer and home Internet service, but we also recommend using an audio interface for superior audio quality. If you already have an audio interface for home recording, you can very likely use the one you have. If not, JamKazam offers a high quality audio package of an audio interface (a small box you connect to your computer via USB cable), a microphone, a mic cable, and a mic stand for just $49.99 (less than our cost). After you have signed up, someone from JamKazam will contact you to schedule a 1:1 help session to help you get set up, to make sure your audio and video gear are working properly in an online session, and to make sure you feel comfortable with the key features you will be using in sessions with students. + <% else %> + Click + here for information on the gear requirements to effectively teach using the JamClass service. When you have + everything you need, + use + this set of help articles as a good step-by-step guide to set up your gear for use with the + JamKazam application. After you have signed up, someone from JamKazam will contact you to schedule a test online + session, in which we will make sure your audio and video gear are working properly in an online session, and to make + sure you feel comfortable with the key features you will be using in sessions with students. + <% end %>

3. Learn About JamClass Features
diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb index 8ae916979..7854d712c 100644 --- a/ruby/lib/jam_ruby/models/charge.rb +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -81,7 +81,7 @@ module JamRuby 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}).deliver + AdminMailer.alerts({subject: subject, body: body}).deliver_now return false end diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 505257dc3..a4f1efb8d 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -95,7 +95,7 @@ module JamRuby end def after_create - if (card_presumed_ok || school_on_school?) && !sent_notices + if (card_presumed_ok || !payment_if_school_on_school?) && !sent_notices send_notices end end @@ -393,8 +393,8 @@ module JamRuby end def requires_teacher_distribution?(target) - if school_on_school? - false + if no_school_on_school_payment? + return false elsif target.is_a?(JamRuby::LessonSession) is_test_drive? || (is_normal? && !is_monthly_payment?) elsif target.is_a?(JamRuby::LessonPackagePurchase) @@ -520,7 +520,17 @@ module JamRuby end end - def distribution_price_in_cents(target) + def distribution_price_in_cents(target, education) + distribution = teacher_distribution_price_in_cents(target) + + if education + (distribution * 0.0625).round + else + distribution + end + end + + def teacher_distribution_price_in_cents(target) if is_single_free? 0 elsif is_test_drive? @@ -759,8 +769,10 @@ module JamRuby if user lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id)) + lesson_booking.same_school_free = lesson_booking.school_on_school_payment? else lesson_booking.same_school = false + lesson_booking.same_school_free = false end # two-way association slots, for before_validation loic in slot to work @@ -779,7 +791,7 @@ module JamRuby end def self.unprocessed(current_user) - LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where('school_id IS NULL') + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where(same_school_free: false) end def self.requested(current_user) @@ -790,6 +802,19 @@ module JamRuby same_school end + def school_on_school_payment? + !!(same_school && school.education) + end + + def no_school_on_school_payment? + !!(school_on_school? && !school_on_school_payment?) + end + + # if this is school-on-school, is payment required? + def payment_if_school_on_school? + !!(!school_on_school? || school_on_school_payment?) + end + def school_and_teacher if school && school.scheduling_comm? [school.communication_email, teacher.email] @@ -862,7 +887,7 @@ module JamRuby .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) - .where(same_school: false) + .where(same_school_free: false) .active .where('music_sessions.scheduled_start >= ?', current_month_first_day) .where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq @@ -932,8 +957,10 @@ module JamRuby 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 + puts "bill_for_month" 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? + puts "bill_for_month - day" current_month_purchase = LessonPackagePurchase.create(user, self, lesson_package_type, day_in_month.year, day_in_month.month) end current_month_purchase.bill_monthly diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index 2f88af5c5..076d14338 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -4,7 +4,9 @@ module JamRuby @@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 + 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 :test_drive_count, to: :lesson_package_type # who purchased the lesson package? @@ -14,7 +16,7 @@ module JamRuby belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id has_one :lesson_session, class_name: "JamRuby::LessonSession", dependent: :destroy - has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + has_many :teacher_distributions, class_name: "JamRuby::TeacherDistribution" has_one :sale_line_item, class_name: "JamRuby::SaleLineItem", dependent: :destroy @@ -35,7 +37,7 @@ module JamRuby end def create_charge - if !school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? + if payment_if_school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? self.lesson_payment_charge = LessonPaymentCharge.new lesson_payment_charge.user = user lesson_payment_charge.amount_in_cents = 0 @@ -45,16 +47,22 @@ module JamRuby end end + def teacher_distribution + teacher_distributions.where(education:false).first + end + + def education_distribution + teacher_distributions.where(education:true).first + end + def add_test_drives if self.lesson_package_type.is_test_drive? new_test_drives = user.remaining_test_drives + lesson_package_type.test_drive_count User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) user.remaining_test_drives = new_test_drives end - end - def to_s "#{name}" end @@ -79,9 +87,14 @@ module JamRuby purchase.recurring = true if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) - purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false) + purchase.teacher_distributions << teacher_dist # price should always match the teacher_distribution, if there is one - purchase.price = purchase.teacher_distribution.amount_in_cents / 100 + purchase.price = teacher_dist.amount_in_cents / 100 + + if lesson_booking.school_on_school_payment? + self.teacher_distributions << TeacherDistribution.create_for_lesson_package_purchase(purchase, true) + end end else purchase.recurring = false @@ -136,10 +149,23 @@ module JamRuby end end + def school_on_school_payment? + !!(school_on_school? && teacher.teacher.school.education) + end + + def no_school_on_school_payment? + !!(school_on_school? && !school_on_school_payment?) + end + + # if this is school-on-school, is payment required? + def payment_if_school_on_school? + !!(!school_on_school? || school_on_school_payment?) + end def bill_monthly(force = false) - if school_on_school? + if school_on_school_payment? + puts "SCHOOL ON SCHOOL PAYMENT OH NO" raise "school-on-school: should not be here" else lesson_payment_charge.charge(force) diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb index c4f5dd508..b4bb267a3 100644 --- a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -63,11 +63,8 @@ module JamRuby post_sale_test_failure - distribution = target.teacher_distribution - if distribution # not all lessons/payment charges have a distribution - distribution.ready = true - distribution.save(validate: false) - end + target.teacher_distributions.update_all(ready:true) # possibly there are 0 distributions on this lesson + stripe_charge end @@ -103,7 +100,9 @@ module JamRuby end def expected_price_in_cents - target.lesson_booking.distribution_price_in_cents(target) + distribution = target.teacher_distribution + for_education = distribution && distribution.education + target.lesson_booking.distribution_price_in_cents(target, for_education) end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index c9de915a8..88a42a2e9 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -11,7 +11,7 @@ module JamRuby @@log = Logging.logger[LessonSession] delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true - delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, to: :lesson_booking + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :school_on_school_payment?, :no_school_on_school_payment?, :payment_if_school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, to: :lesson_booking delegate :pretty_scheduled_start, to: :music_session @@ -41,7 +41,7 @@ module JamRuby belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id, :dependent => :destroy belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson, :dependent => :destroy - has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution", dependent: :destroy + has_many :teacher_distributions, class_name: "JamRuby::TeacherDistribution", dependent: :destroy has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id" has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" @@ -86,7 +86,7 @@ module JamRuby .order('music_sessions.scheduled_start DESC') } def create_charge - if !school_on_school? && !is_test_drive? && !is_monthly_payment? + if payment_if_school_on_school? && !is_test_drive? && !is_monthly_payment? self.lesson_payment_charge = LessonPaymentCharge.new lesson_payment_charge.user = @assigned_student lesson_payment_charge.amount_in_cents = 0 @@ -96,6 +96,14 @@ module JamRuby end end + def teacher_distribution + teacher_distributions.where(education:false).first + end + + def education_distribution + teacher_distributions.where(education:true).first + end + def manage_slot_changes # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . # TODO: what to do, what to do. @@ -209,7 +217,10 @@ module JamRuby self.status = STATUS_COMPLETED if success && lesson_booking.requires_teacher_distribution?(self) - self.teacher_distribution = TeacherDistribution.create_for_lesson(self) + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) + if lesson_booking.school_on_school_payment? + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true) + end end if self.save @@ -292,7 +303,7 @@ module JamRuby end def bill_lesson - if school_on_school? + if no_school_on_school_payment? success = true else lesson_payment_charge.charge @@ -341,14 +352,9 @@ module JamRuby def test_drive_completed - distribution = teacher_distribution - if !sent_notices if success - if distribution # not all lessons/payment charges have a distribution - distribution.ready = true - distribution.save(validate: false) - end + teacher_distributions.update_all(ready:true) # possibly there are 0 distributions on this lesson student.test_drive_succeeded(self) else student.test_drive_failed(self) @@ -387,7 +393,7 @@ module JamRuby else if lesson_booking.is_monthly_payment? if !sent_notices - if !school_on_school? + if payment_if_school_on_school? # bad session; just poke user UserMailer.monthly_recurring_no_bill(self).deliver_now end @@ -401,7 +407,7 @@ module JamRuby else if !sent_notices - if !school_on_school? + if payment_if_school_on_school? # bad session; just poke user UserMailer.student_lesson_normal_no_bill(self).deliver_now end @@ -422,7 +428,7 @@ module JamRuby bill_lesson else if !sent_notices - if !school_on_school? + if payment_if_school_on_school? UserMailer.student_lesson_normal_no_bill(self).deliver_now UserMailer.teacher_lesson_normal_no_bill(self).deliver_now end diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb index 1e7523b50..94aaea30e 100644 --- a/ruby/lib/jam_ruby/models/school.rb +++ b/ruby/lib/jam_ruby/models/school.rb @@ -24,6 +24,7 @@ module JamRuby validates :user, presence: true validates :enabled, inclusion: {in: [true, false]} + validates :education, inclusion: {in: [true, false]} validates :scheduling_communication, inclusion: {in: SCHEDULING_COMMS} validates :correspondence_email, email: true, allow_blank: true validate :validate_avatar_info @@ -31,6 +32,10 @@ module JamRuby after_create :create_affiliate before_save :stringify_avatar_info, :if => :updating_avatar + def is_education? + education + end + def scheduling_comm? scheduling_communication == SCHEDULING_COMM_SCHOOL end @@ -39,6 +44,10 @@ module JamRuby correspondence_email.blank? ? owner.email : correspondence_email end + def approved_teachers + teachers.where('teachers.ready_for_session_at is not null') + end + def create_affiliate AffiliatePartner.create_from_school(self) end @@ -119,4 +128,8 @@ module JamRuby self.crop_selection = crop_selection.to_json if !crop_selection.nil? end end + + def teacher_list_url + "#{APP_CONFIG.external_root_url}/school/#{id}/teachers" + end end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index 14d1fe2f5..126847f48 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -416,21 +416,7 @@ module JamRuby ## !!!! this is only valid for tests def stripe_account_id=(new_acct_id) - existing = user.stripe_auth - existing.destroy if existing - - user_auth_hash = { - :provider => 'stripe_connect', - :uid => new_acct_id, - :token => 'bogus', - :refresh_token => 'refresh_bogus', - :token_expiration => Date.new(2050, 1, 1), - :secret => "secret" - } - - authorization = user.user_authorizations.build(user_auth_hash) - authorization.save! - + user.stripe_account_id = new_acct_id end # how complete is their profile? diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 5d93d3581..51c964329 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -43,24 +43,26 @@ module JamRuby end end - def self.create_for_lesson(lesson_session) - distribution = create(lesson_session) + def self.create_for_lesson(lesson_session, for_education) + distribution = create(lesson_session, for_education) distribution.lesson_session = lesson_session + distribution.education = for_education distribution end - def self.create_for_lesson_package_purchase(lesson_package_purchase) - distribution = create(lesson_package_purchase) + def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education) + distribution = create(lesson_package_purchase, for_education) distribution.lesson_package_purchase = lesson_package_purchase + distribution.education = for_education distribution end - def self.create(target) + def self.create(target, education) distribution = TeacherDistribution.new distribution.teacher = target.teacher distribution.ready = false distribution.distributed = false - distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target) + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education) distribution.school = target.lesson_booking.school distribution end @@ -82,6 +84,7 @@ module JamRuby end def real_distribution + (real_distribution_in_cents / 100.0) end @@ -109,17 +112,21 @@ module JamRuby end def calculate_teacher_fee - if is_test_drive? + if education 0 else - if school - # if school exists, use it's rate - rate = school.jamkazam_rate + if is_test_drive? + 0 else - # otherwise use the teacher's rate - rate = teacher.teacher.jamkazam_rate + if school + # if school exists, use it's rate + rate = school.jamkazam_rate + else + # otherwise use the teacher's rate + rate = teacher.teacher.jamkazam_rate + end + (amount_in_cents * (rate + 0.03)).round end - (amount_in_cents * (rate + 0.03)).round end end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 25ac5f614..44bb299c7 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -15,7 +15,11 @@ module JamRuby # pay the school if the payment owns the school; otherwise default to the teacher def payable_teacher if school - school.owner + if school.education + teacher + else + school.owner + end else teacher end diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb index 3d74167b7..ce52fd143 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -21,8 +21,6 @@ module JamRuby 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", diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 4faf1d925..7b098b08c 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -2033,6 +2033,24 @@ module JamRuby customer end + ## !!!! this is only valid for tests + def stripe_account_id=(new_acct_id) + existing = stripe_auth + existing.destroy if existing + + user_auth_hash = { + :provider => 'stripe_connect', + :uid => new_acct_id, + :token => 'bogus', + :refresh_token => 'refresh_bogus', + :token_expiration => Date.new(2050, 1, 1), + :secret => "secret" + } + + authorization = user_authorizations.build(user_auth_hash) + authorization.save! + end + def card_approved(token, zip, booking_id, test_drive_package_choice_id = nil) approved_booking = nil @@ -2261,6 +2279,10 @@ module JamRuby LessonBooking.engaged_bookings(student, self, since_at).test_drive.count > 0 end + def same_school_with_student?(student) + student.school && self.teacher && self.teacher.school && student.school.id == self.teacher.school.id + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb index 1f8ca34a1..fee6bf527 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -35,6 +35,8 @@ describe "Monthly Recurring Lesson Flow" do booking.card_presumed_ok.should be_false booking.user.should eql user booking.card_presumed_ok.should be_false + booking.same_school.should be_false + booking.same_school_free.should be_false booking.should eql user.unprocessed_normal_lesson booking.sent_notices.should be_false booking.booked_price.should eql 30.00 @@ -213,6 +215,252 @@ describe "Monthly Recurring Lesson Flow" do + # 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 = 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 nil + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 0 # one for student + end + + it "works (school on school education)" do + + # make sure teacher can get payments + teacher.stripe_account_id = stripe_account1_id + school.user.stripe_account_id = stripe_account2_id + + # get user and teacher into same school + school.education = true + school.save! + user.school = school + user.save! + teacher.school = school + teacher.save! + + + # 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_id: booking.id}) + booking.reload + booking.card_presumed_ok.should be_true + booking.errors.any?.should be_false + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + lesson.errors.any?.should be_false + + 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_recurring, 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 + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, 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, accepter: teacher_user}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 2 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + 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 + + # let user pay for it + LessonBooking.hourly_check + + booked_price = booking.booked_price + prorated = booked_price / 2 + prorated_cents = (booked_price * 100).to_i + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + puts "LESSON_PURCHASE PRICE #{lesson_purchase.price}" + lesson_purchase.price.should eql prorated + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql prorated_cents + teacher_distribution = lesson_purchase.teacher_distribution + teacher_distribution.amount_in_cents.should eql prorated_cents + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + education_distribution = lesson_purchase.education_distribution + education_distribution.amount_in_cents.should eql (prorated_cents * 0.0625).round + education_distribution.ready.should be_true + education_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 * prorated * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((prorated * 100 * 0.0825).round + 100 * prorated).to_i + sale.recurly_subtotal_in_cents.should eql prorated_cents + 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.hourly_check + teacher_distribution.reload + teacher_distribution.distributed.should be_true + TeacherPayment.count.should eql 2 + payment = teacher_distribution.teacher_payment + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + education_distribution.reload + education_distribution.distributed.should be_true + + education_amt = (3000 * 0.0625).round + payment = education_distribution.teacher_payment + payment.amount_in_cents.should eql education_amt + payment.fee_in_cents.should eql 0 + payment.teacher_payment_charge.amount_in_cents.should eql (education_amt + education_amt * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql education_distribution + + + # teacher & student get into session start = lesson_session.scheduled_start end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) @@ -422,6 +670,7 @@ describe "Monthly Recurring Lesson Flow" do end + it "affiliate gets their cut" do Timecop.travel(2016, 05, 15) user.affiliate_referral = affiliate_partner diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb index da9a65fe7..71192bccc 100644 --- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -266,12 +266,16 @@ describe "Normal Lesson Flow" do it "works" do + # set up teacher stripe acct + teacher.stripe_account_id = stripe_account1_id + # 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.same_school_free.should be_false booking.should eql user.unprocessed_normal_lesson booking.sent_notices.should be_false booking.booked_price.should eql 30.00 @@ -293,6 +297,7 @@ describe "Normal Lesson Flow" do user.stripe_customer_id.should_not be nil user.remaining_test_drives.should eql 0 user.lesson_purchases.length.should eql 0 + teacher_user.stripe_auth.should_not be_nil customer = Stripe::Customer.retrieve(user.stripe_customer_id) customer.email.should eql user.email @@ -389,6 +394,7 @@ describe "Normal Lesson Flow" do UserMailer.deliveries.clear # background code comes around and analyses the session LessonSession.hourly_check + lesson_session.reload lesson_session.analysed.should be_true analysis = lesson_session.analysis @@ -397,6 +403,17 @@ describe "Normal Lesson Flow" do 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 + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 1 + + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + education_distribution = lesson_session.education_distribution + education_distribution.should be_nil lesson_session.billed.should be true user.reload user.lesson_purchases.length.should eql 1 @@ -423,7 +440,7 @@ describe "Normal Lesson Flow" do 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 + UserMailer.deliveries.length.should eql 3 # one for student, one for teacher end @@ -568,6 +585,224 @@ describe "Normal Lesson Flow" do TeacherDistribution.count.should eql 0 end + + it "works (school on school education)" do + + # make sure teacher can get payments + teacher.stripe_account_id = stripe_account1_id + school.user.stripe_account_id = stripe_account2_id + + # make sure can get stripe payments + + # get user and teacher into same school + + school.education = true + school.save! + user.school = school + user.save! + teacher.school = school + teacher.save! + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.school.should be_true + booking.card_presumed_ok.should be_false + booking.user.should eql user + user.unprocessed_normal_lesson.should be_nil + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + booking.is_requested?.should be_true + booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + LessonPaymentCharge.count.should eql 1 + + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + Timecop.travel(end_time + 1) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be_true + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 1 + + LessonBooking.hourly_check + + lesson_session.reload + 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_session.teacher_distributions.count.should eql 2 + education_distribution = lesson_session.education_distribution + education_distribution.amount_in_cents.should eql (3000 * 0.0625).round + education_distribution.ready.should be_true + education_distribution.distributed.should be_false + + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + user.sales.length.should eql 1 + lesson_session.amount_charged.should eql 32.48 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be_true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 2 + + LessonPaymentCharge.count.should eql 1 + TeacherDistribution.count.should eql 2 + + + teacher_distribution.reload + teacher_distribution.distributed.should be_true + education_distribution.reload + education_distribution.distributed.should be_true + + education_amt = (3000 * 0.0625).round + payment = education_distribution.teacher_payment + payment.amount_in_cents.should eql education_amt + payment.fee_in_cents.should eql 0 + payment.teacher_payment_charge.amount_in_cents.should eql (education_amt + education_amt * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql education_distribution + payment = teacher_distribution.teacher_payment + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true + + + + end + it "affiliate gets their cut" do user.affiliate_referral = affiliate_partner user.save! diff --git a/web/app/assets/images/landing/Scott Himel - Avatar.png b/web/app/assets/images/landing/Scott Himel - Avatar.png new file mode 100644 index 000000000..4b8123816 Binary files /dev/null and b/web/app/assets/images/landing/Scott Himel - Avatar.png differ diff --git a/web/app/assets/images/landing/Scott Himel - Speech Bubble.png b/web/app/assets/images/landing/Scott Himel - Speech Bubble.png new file mode 100644 index 000000000..06dc9110e Binary files /dev/null and b/web/app/assets/images/landing/Scott Himel - Speech Bubble.png differ diff --git a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee index 6d43f38ea..73dc99586 100644 --- a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee @@ -92,7 +92,8 @@ profileUtils = context.JK.ProfileUtils schoolName: null, studentInvitations: null, teacherInvitations: null, - updating: false + updating: false, + distributions: [] } isSchoolManaged: () -> @@ -187,9 +188,15 @@ profileUtils = context.JK.ProfileUtils removeFromSchool: (id, isTeacher, e) -> if isTeacher - rest.deleteSchoolTeacher({id: this.state.school.id, teacher_id: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + 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: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + rest.deleteSchoolStudent({ + id: this.state.school.id, + student_id: id + }).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) removeFromSchoolDone: (school) -> context.JK.Banner.showNotice("User removed", "User was removed from your school.") @@ -205,7 +212,7 @@ profileUtils = context.JK.ProfileUtils `

- +
{user.name} @@ -237,7 +244,8 @@ profileUtils = context.JK.ProfileUtils if this.state.school.teachers? && this.state.school.teachers.length > 0 for teacher in this.state.school.teachers - teachers.push(@renderUser(teacher.user, true)) + if teacher.user + teachers.push(@renderUser(teacher.user, true)) else teachers = `

No teachers

` @@ -302,8 +310,39 @@ profileUtils = context.JK.ProfileUtils field: true }) - cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } - updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + cancelClasses = {"button-grey": true, "cancel": true, disabled: this.state.updating} + updateClasses = {"button-orange": true, "update": true, disabled: this.state.updating} + + if this.state.school.education + management = null + else + management = `
+

Management Preference

+ +
+
+ +
+
+ +
+
+
+ + + +
All emails relating to lesson scheduling will go to this email if school owner manages + scheduling. +
+ {correspondenceEmailErrors} +
+
` + `
@@ -316,29 +355,7 @@ profileUtils = context.JK.ProfileUtils
-

Management Preference

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

Payments

@@ -365,7 +382,7 @@ profileUtils = context.JK.ProfileUtils

teachers:

INVITE TEACHER -
+
{teacherInvitations} @@ -397,6 +414,73 @@ profileUtils = context.JK.ProfileUtils

Coming soon

` + paymentsToYou: () -> + rows = [] + + for paymentHistory in this.state.distributions + paymentMethod = 'Stripe' + + if paymentHistory.distributed + date = paymentHistory.teacher_payment.teacher_payment_charge.last_billing_attempt_at + status = 'Paid' + else + date = paymentHistory.created_at + if paymentHistory.not_collectable + status = 'Uncollectible' + else if !paymentHistory.teacher?.teacher?.stripe_account_id? + status = 'No Stripe Acct' + else + status = 'Collecting' + + + date = context.JK.formatDate(date, true) + description = paymentHistory.description + + if paymentHistory.teacher_payment? + amt = paymentHistory.teacher_payment.real_distribution_in_cents + else + amt = paymentHistory.real_distribution_in_cents + + displayAmount = ' $' + (amt / 100).toFixed(2) + + amountClasses = {status: status} + + row = + ` + {date} + {paymentMethod} + {description} + {status} + {displayAmount} + ` + rows.push(row) + + `
+ + + + + + + + + + + + {rows} + + +
DATEMETHODDESCRIPTIONSTATUSAMOUNT
+ Next + +
No more payment history
+
+ BACK +
+
+
` + + agreement: () -> `

The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the diff --git a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee index f19dce5ee..018a9bfc7 100644 --- a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee @@ -99,7 +99,7 @@ UserStore = context.UserStore userDetailDone: (response) -> if response.id == @state.teacherId - school_on_school = response.teacher.school_id? && @state.user?.school_id? && response.teacher.school_id == @state.user.school_id + school_on_school = response.teacher.school_id? && @state.user?.school_id? && response.teacher.school_id == @state.user.school_id && !response.teacher.school.education @setState({teacher: response, isSelf: response.id == context.JK.currentUserId, school_on_school: school_on_school}) else logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId) diff --git a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee index b83d3a4c4..ff6969219 100644 --- a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee @@ -1,8 +1,9 @@ context = window +SchoolStore = context.SchoolStore @InviteSchoolUserDialog = React.createClass({ - mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(SchoolStore, "onSchoolChanged")] teacher: false beforeShow: (args) -> @@ -14,6 +15,9 @@ context = window @setState({inviteErrors: null, teacher: args.d1}) afterHide: () -> + onSchoolChanged: (schoolState) -> + @setState(schoolState) + onAppInit: (@app) -> dialogBindings = { 'beforeShow': @beforeShow, @@ -22,12 +26,11 @@ context = window @app.bindDialog('invite-school-user', dialogBindings); - componentDidMount: () -> @root = $(@getDOMNode()) getInitialState: () -> - {inviteErrors: null} + {inviteErrors: null, school: null} doCancel: (e) -> e.preventDefault() @@ -41,15 +44,20 @@ context = window firstName = @root.find('input[name="first_name"]').val() school = context.SchoolStore.getState().school @setState({inviteErrors: null}) - rest.createSchoolInvitation({id: school.id, as_teacher: this.state.teacher, email: email, last_name: lastName, first_name: firstName }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) + rest.createSchoolInvitation({ + id: school.id, + as_teacher: this.state.teacher, + email: email, + last_name: lastName, + first_name: firstName + }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) - createDone:(response) -> + createDone: (response) -> context.SchoolActions.addInvitation(response) context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!") @app.layout.closeDialog('invite-retailer-user') createFail: (jqXHR) -> - handled = false if jqXHR.status == 422 @@ -60,15 +68,57 @@ context = window if !handled @app.ajaxError(jqXHR, null, null) - render: () -> + renderEducation: () -> + `

+
+ +

How to Invite Your Students

+
+
+ +

+ Please copy and paste the text below into the email application you use to communicate with students and + parents in your music program. This is a suggested starting point, but you may edit the message as you prefer. + Please make sure the web page link in this message is included in the email you send and is unchanged because + students must use this specific link to sign up so that they will be properly associated with your school. +

+ + + +
+ DONE +
+
+
` + + close: (e) -> + e.preventDefault() + @app.layout.closeDialog('invite-school-user'); + + educationCopyEmailText: () -> + path = context.JK.makeAbsolute("/school/#{this.state.school.id}/student") + + msg = "Hello Students & Parents - + +I'm writing to make you aware of a very interesting new option for private music lessons. A company called JamKazam has built remarkable new technology that lets musicians play together live in sync with studio quality audio from different locations over the Internet. Here's an example: https://www.youtube.com/watch?v=I2reeNKtRjg. Now they have built an online music lesson service that uses this technology: https://www.youtube.com/watch?v=wdMN1fQyD9k. + +\n\nThis means that students can now take lessons online and much more conveniently from home. Parents don't have to leave work early to drive students to and from lessons during rush hour. A 30-minute lesson is just a 30-minute lesson at home, not a 90-minute expedition across town. And students can record lessons to refer back to them later. + +\n\nIf the convenience of online lessons is attractive to your family, then you can use this link to sign up for online lessons: #{path}. After you sign up, someone from JamKazam will reach out to answer your questions and help you get set up and ready to go. Your student can continue to take lessons from the same instructor through this service if desired. The student will need access to a Windows or Mac computer, and you'll need basic Internet service at home. The service uses the built-in microphone and headphone jack on the computer for audio. You can also purchase a pro audio upgrade package from JamKazam for $49.99 that includes an audio interface (a small box that connects to the computer via a USB cable), a microphone, a microphone cable, and a microphone stand. This is optional, but will deliver superior audio quality in lessons. + +\n\nThe music program directors are primarily concerned with giving our students the highest quality music education possible, so we encourage you to make whatever decision you feel is best for the student. That said, for students who take lessons through the JamKazam service, a portion of the lesson fees are distributed back into our music program booster fund, which helps to fund the program's expenses, and is a nice additional benefit. If you have more questions, you can send an email to support@jamkazam.com." + + return msg + + renderSchool: () -> firstNameErrors = context.JK.reactSingleFieldErrors('first_name', @state.inviteErrors) lastNameErrors = context.JK.reactSingleFieldErrors('last_name', @state.inviteErrors) emailErrors = context.JK.reactSingleFieldErrors('email', @state.inviteErrors) - firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true}) - lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true}) - emailClasses = classNames({email: true, error: emailErrors?, field: true}) + firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true}) + lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true}) + emailClasses = classNames({email: true, error: emailErrors?, field: true}) if @state.teacher title = 'invite teacher' @@ -98,19 +148,19 @@ context = window
- + {firstNameErrors}
- + {lastNameErrors}
- + {emailErrors}
@@ -121,4 +171,16 @@ context = window
` + render: () -> + school = this.state.school + + if !school? + return `
no school
` + + if school.education + @renderEducation() + else + @renderSchool() + + }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee index 71c25bdd6..0e1177774 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -145,7 +145,9 @@ proficiencyDescriptionMap = { if @state.user?['has_booked_test_drive_with_student'] @showBuyNormalLessonBubble() else - if @user['remaining_test_drives'] > 0 + if @state.user?['same_school_with_student'] + @showBuyNormalLessonBubble() + else if @user['remaining_test_drives'] > 0 @showUseRemainingTestDrivesBubble() else if @user['can_buy_test_drive?'] @showBuyTestDriveBubble() diff --git a/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee new file mode 100644 index 000000000..2aee0cc63 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee @@ -0,0 +1,378 @@ +context = window +rest = context.JK.Rest() + +@JamClassEducationLandingBottomPage = React.createClass({ + + render: () -> + `
+
+

How JamClass by JamKazam Can Help Your Music School

+ +

Online music lessons offer major advantages to your students, private lesson teachers, and your school's + booster program.

+ +

+ Students can take lessons much more conveniently from home, while enjoying studio quality audio and while + retaining the ability to play live in sync with their instructor. Students can take lessons from the best + teacher vs. settling for someone who lives close by. Parents don't have to leave work early to drive students + to and from lessons during rush hour, while carting siblings along to lessons. A 30-minute lesson is just a + 30-minute lesson, not a 90-minute expedition. And students can record lessons to refer back to them later. +

+ +

+ Teachers can now provide lessons to students nearly anywhere, rather than being constrained to students who + live within a 30-minute drive. Teachers don't have to spend as much time driving to schools and to students' + homes as they do teaching, so they can travel less, teach more, and earn more. And teachers can provide + instruction to students from underserved schools that are located in areas that are more difficult to reach. +

+ + +

+ Even the booster program benefits, as JamKazam funnels a portion of lesson fees back into the music program + booster fund, helping to pay for trips, instrument repairs, and other music program expenses - all without + students selling things, and without additional time or effort expended by the music program director. +

+ +

Some teachers and students have historically tried using Skype to power online lessons, but have found that + the lesson experience is significantly diminished. Why? Because Skype and similar apps were built for voice + chat – not to deliver online music lessons. This is a major problem. Voice technology processes all audio as + if it were a spoken human voice, which makes music sound awful in online sessions – so bad that teachers can’t + assess the student’s tone and sometimes even the pitch of what they are playing. These apps also have very + high latency – a technical term that means that the student and teacher cannot play together, another + important requirement for productive lessons. Since Skype wasn’t built for music, it also lacks many other + basic features to support effective lessons, like a metronome, mixers, backing tracks, etc. +

+ +

+ At JamKazam, we’ve spent years designing, patenting, and building technology specifically to enable musicians + to play online live in sync with studio quality audio. We’ve built a wide variety of critical online music + performance features into this platform. And now we’ve built a lesson marketplace on top of this foundation, + and crafted a partner program specifically to meet the needs of secondary education music programs. The bottom + line is that your students, private lesson teachers, and your music program's booster fund can now all "win" + by adopting this amazing new Internet service. And you don't have to do it all at once. You can simply make + this available as an option to students and parents who decide this is a good fit for them and will help them. +

+ +

+ If this sounds interesting to you, read on to learn more about some of the top features of JamClass by + JamKazam. +

+ +
+

JamClass Kudos

+ +
+ + +

Scott Himel

+ +
+ Texas high school band director +
+
+
+ + +

Justin Pierce

+ +
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor +
+
+
+ + +

Dave Sebree

+ +
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician +
+
+
+ + +

Sara Nelson

+ +
+ Cellist for Austin Lyric Opera, frequently recorded with major artists +
+
+
+
+ +
+
+

+
1
+ Play Live In Sync From Different Locations +

+

+

+
+