983 lines
34 KiB
Ruby
983 lines
34 KiB
Ruby
# represenst the type of lesson package
|
|
module JamRuby
|
|
class LessonSession < ActiveRecord::Base
|
|
|
|
include HtmlSanitize
|
|
html_sanitize strict: [:cancel_message]
|
|
|
|
attr_accessor :accepting, :creating, :countering, :autocanceling, :countered_slot, :countered_lesson, :canceling, :assigned_student
|
|
|
|
|
|
@@log = Logging.logger[LessonSession]
|
|
|
|
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_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 :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_UNCONFIRMED = 'unconfirmed'
|
|
|
|
STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED, STATUS_UNCONFIRMED]
|
|
|
|
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", :dependent => :destroy
|
|
belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons
|
|
belongs_to :canceler, class_name: "JamRuby::User", foreign_key: :canceler_id
|
|
belongs_to :counterer, class_name: "JamRuby::User", foreign_key: :counterer_id
|
|
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
|
|
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
|
|
belongs_to :user, class_name: "JamRuby::User"
|
|
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"
|
|
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
|
|
has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id"
|
|
has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id"
|
|
|
|
|
|
validates :duration, presence: true, numericality: {only_integer: true}
|
|
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
|
|
validate :validate_autocancel, :if => :autocanceling
|
|
|
|
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) }
|
|
scope :upcoming, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', Time.now) }
|
|
scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now) }
|
|
# show all requested/countered sessions where the student was the last to communicate
|
|
scope :slow_responses, -> { joins(:lesson_booking).where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED)
|
|
.where('lesson_bookings.counterer_id IS NULL OR lesson_bookings.user_id = lesson_bookings.counterer_id')
|
|
.order('(COALESCE(lesson_bookings.countered_at, lesson_bookings.sent_notices_at)) ASC') }
|
|
scope :least_time_left, -> { joins(:lesson_booking, :music_session).where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED)
|
|
.where('lesson_bookings.counterer_id IS NULL OR lesson_bookings.user_id = lesson_bookings.counterer_id')
|
|
.order('music_sessions.scheduled_start DESC') }
|
|
|
|
def create_charge
|
|
if !school_on_school? && !is_test_drive? && !is_monthly_payment?
|
|
self.lesson_payment_charge = LessonPaymentCharge.new
|
|
lesson_payment_charge.user = @assigned_student
|
|
lesson_payment_charge.amount_in_cents = 0
|
|
lesson_payment_charge.fee_in_cents = 0
|
|
lesson_payment_charge.lesson_session = self
|
|
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 music_session_id
|
|
music_session.id
|
|
end
|
|
|
|
def self.hourly_check
|
|
auto_cancel
|
|
analyse_sessions
|
|
complete_sessions
|
|
remind_counters
|
|
end
|
|
|
|
def self.minutely_check
|
|
upcoming_sessions_reminder
|
|
end
|
|
|
|
def self.remind_counters
|
|
MusicSession.joins(lesson_session: :lesson_booking)
|
|
.where("lesson_bookings.sent_counter_reminder = false")
|
|
.where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED)
|
|
.where("? > (COALESCE(lesson_bookings.countered_at, lesson_bookings.sent_notices_at)) + (INTERVAL '24 hours')", Time.now).each do |music_session|
|
|
lesson_session = music_session.lesson_session
|
|
if lesson_session.student_last_proposed?
|
|
UserMailer.teacher_counter_reminder(lesson_session).deliver!
|
|
else
|
|
UserMailer.student_counter_reminder(lesson_session).deliver!
|
|
end
|
|
lesson_session.lesson_booking.sent_counter_reminder = true
|
|
lesson_session.lesson_booking.save(validate: false)
|
|
end
|
|
end
|
|
|
|
def self.auto_cancel
|
|
MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).each do |music_session|
|
|
lesson_session = music_session.lesson_session
|
|
lesson_session.autocancel
|
|
end
|
|
end
|
|
|
|
def self.analyse_sessions
|
|
MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).where('analysed = false').each do |music_session|
|
|
lesson_session = music_session.lesson_session
|
|
lesson_session.analyse
|
|
end
|
|
end
|
|
|
|
def self.complete_sessions
|
|
# this will find any paid session (recurring monthly paid, recurring single paid, single paid)
|
|
MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where("? > scheduled_start + (INTERVAL '1 minutes' * (duration))", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session|
|
|
lesson_session = music_session.lesson_session
|
|
lesson_session.session_completed
|
|
end
|
|
|
|
# test drives don't have a lesson_payment_charge, so we don't join against them
|
|
MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_COMPLETED).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("? > scheduled_start + (INTERVAL '1 minutes' * duration)", Time.now).where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session|
|
|
lession_session = music_session.lesson_session
|
|
lession_session.session_completed
|
|
end
|
|
end
|
|
|
|
def self.upcoming_sessions_reminder
|
|
now = Time.now
|
|
half_hour_from_now = 30.minutes.from_now
|
|
if Time.zone
|
|
now = Time.zone.local_to_utc(now)
|
|
half_hour_from_now = Time.zone.local_to_utc(half_hour_from_now)
|
|
end
|
|
|
|
MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.status = ?', LessonSession::STATUS_APPROVED).where('sent_starting_notice = false').where('(scheduled_start > ? and scheduled_start < ?)', now, half_hour_from_now).each do |music_session|
|
|
lession_session = music_session.lesson_session
|
|
lession_session.send_starting_notice
|
|
end
|
|
end
|
|
|
|
def student_last_proposed?
|
|
counterer_id.nil? || counterer_id == student_id
|
|
end
|
|
|
|
def analyse
|
|
if self.analysed
|
|
return
|
|
end
|
|
|
|
analysis = LessonSessionAnalyser.analyse(self)
|
|
|
|
self.analysis = LessonSession.analysis_to_json(analysis)
|
|
|
|
if analysis[:reason] == LessonSessionAnalyser::SESSION_ONGOING
|
|
# extra protection against bad code somewhere
|
|
return
|
|
end
|
|
self.success = analysis[:bill]
|
|
self.analysed_at = Time.now
|
|
self.analysed = true
|
|
|
|
self.status = STATUS_COMPLETED
|
|
|
|
if success && lesson_booking.requires_teacher_distribution?(self)
|
|
self.teacher_distribution = TeacherDistribution.create_for_lesson(self)
|
|
end
|
|
|
|
if self.save
|
|
# send out emails appropriate for this type of session
|
|
session_completed
|
|
end
|
|
end
|
|
|
|
def billed
|
|
if lesson_booking.is_test_drive?
|
|
false
|
|
elsif lesson_payment_charge
|
|
lesson_payment_charge.billed
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def amount_charged
|
|
if lesson_payment_charge
|
|
lesson_payment_charge.amount_in_cents / 100.0
|
|
else
|
|
0.0
|
|
end
|
|
end
|
|
|
|
def self.analysis_to_json(analysis, preserve_object = false)
|
|
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
|
|
if preserve_object
|
|
json
|
|
else
|
|
json.to_json
|
|
end
|
|
end
|
|
|
|
def send_starting_notice
|
|
UserMailer.lesson_starting_soon_student(self).deliver!
|
|
UserMailer.lesson_starting_soon_teacher(self).deliver!
|
|
|
|
self.sent_starting_notice = true
|
|
self.save(validate: false)
|
|
end
|
|
|
|
def session_completed
|
|
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
|
|
if school_on_school?
|
|
success = true
|
|
else
|
|
lesson_payment_charge.charge
|
|
success = lesson_payment_charge.billed
|
|
end
|
|
|
|
if success
|
|
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 student_missed
|
|
analysed && !success && analysis_json["student_analysis"]["missed"]
|
|
end
|
|
|
|
def teacher_missed
|
|
analysed && !success && analysis_json["teacher_analysis"]["missed"]
|
|
end
|
|
|
|
def error_display
|
|
if analysed
|
|
if !success
|
|
if student_missed && teacher_missed
|
|
"both the student and teacher missed the lesson"
|
|
elsif teacher_missed
|
|
"the teacher missed the lesson"
|
|
elsif student_missed
|
|
"the student missed the lesson"
|
|
else
|
|
"unknown error reason"
|
|
end
|
|
|
|
else
|
|
"because no error"
|
|
end
|
|
else
|
|
"not yet analysed"
|
|
end
|
|
|
|
end
|
|
|
|
|
|
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
|
|
student.test_drive_succeeded(self)
|
|
else
|
|
student.test_drive_failed(self)
|
|
end
|
|
|
|
self.lesson_booking.success = success
|
|
self.lesson_booking.status = STATUS_COMPLETED
|
|
self.lesson_booking.save(:validate => false)
|
|
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
|
|
puts "RECURRING COMPLETED #{success}"
|
|
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?
|
|
if !sent_notices
|
|
if !school_on_school?
|
|
# bad session; just poke user
|
|
UserMailer.monthly_recurring_no_bill(self).deliver
|
|
end
|
|
|
|
self.sent_notices = true
|
|
self.sent_notices_at = Time.now
|
|
self.post_processed = true
|
|
self.post_processed_at = Time.now
|
|
self.save(:validate => false)
|
|
end
|
|
|
|
else
|
|
puts "STUDENT NO BILL SENT #{self.id}"
|
|
if !sent_notices
|
|
if !school_on_school?
|
|
# bad session; just poke user
|
|
UserMailer.student_lesson_normal_no_bill(self).deliver
|
|
end
|
|
|
|
self.sent_notices = true
|
|
self.sent_notices_at = Time.now
|
|
self.post_processed = true
|
|
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
|
|
if !school_on_school?
|
|
puts "STUDENT NO BILL SENT #{success}"
|
|
UserMailer.student_lesson_normal_no_bill(self).deliver
|
|
UserMailer.teacher_lesson_normal_no_bill(self).deliver
|
|
end
|
|
|
|
self.sent_notices = true
|
|
self.sent_notices_at = Time.now
|
|
self.post_processed = true
|
|
self.post_processed_at = Time.now
|
|
self.save(:validate => false)
|
|
end
|
|
end
|
|
|
|
self.lesson_booking.success = success
|
|
self.lesson_booking.status = STATUS_COMPLETED
|
|
self.lesson_booking.save(:validate => false)
|
|
end
|
|
|
|
def after_counter
|
|
send_counter(@countered_lesson, @countered_slot)
|
|
end
|
|
|
|
def scheduled_start
|
|
if music_session
|
|
music_session.scheduled_start
|
|
else
|
|
raise "lesson session #{id} has no music session in scheduled_start"
|
|
end
|
|
|
|
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_approved?
|
|
status == STATUS_APPROVED
|
|
end
|
|
|
|
def is_suspended?
|
|
status == STATUS_SUSPENDED
|
|
end
|
|
|
|
def is_countered?
|
|
status == STATUS_COUNTERED
|
|
end
|
|
|
|
def analysis_json
|
|
@parsed_analysis || analysis ? JSON.parse(analysis) : nil
|
|
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 this is a single lesson (testdrive, paid), but the scheduled time is before now, then it's in the past, and we have to reject
|
|
if !recurring && self.slot.scheduled_time(0) <= Time.now
|
|
self.errors.add(:slot, "is in the past")
|
|
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_autocancel
|
|
|
|
if scheduled_start + (duration * 60) > Time.now
|
|
self.errors.add(:status, "This session is not in the past.")
|
|
end
|
|
|
|
if self.status_was != STATUS_REQUESTED
|
|
self.errors.add(:status, "This session is #{self.status_was} and can not be autocanceled")
|
|
end
|
|
|
|
self.autocanceling = 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
|
|
lesson_session.assigned_student = booking.student
|
|
lesson_session.user = booking.student
|
|
if booking.is_test_drive? && booking.student.remaining_test_drives > 0
|
|
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 student_id
|
|
music_session.creator.id
|
|
end
|
|
|
|
def self.index(user, params = {})
|
|
limit = params[:per_page]
|
|
limit ||= 100
|
|
limit = limit.to_i
|
|
|
|
school_owner = user.owned_school && user.owned_school.scheduling_comm?
|
|
|
|
is_teacher = !user.teacher.nil?
|
|
|
|
query = LessonSession.unscoped.joins([:music_session, :lesson_booking]).joins(music_session: :creator)
|
|
#query = query.includes([:teacher, :music_session])
|
|
query = query.includes([:music_session])
|
|
query = query.order('music_sessions.scheduled_start DESC')
|
|
|
|
|
|
if params[:as_teacher].present?
|
|
if school_owner || params[:as_teacher] == true || params[:as_teacher] == "true"
|
|
|
|
# if a school owner is viewing, grab his owned school ID. Otherwise use a teacher's school ID, if applicable
|
|
school_id = school_owner ? user.owned_school.id : (user.teacher ? user.teacher.school_id : nil)
|
|
|
|
if school_id
|
|
school_extra = "OR (lesson_bookings.school_id = '#{school_id}')"
|
|
else
|
|
school_extra = ''
|
|
end
|
|
|
|
if school_owner
|
|
extra_teacher= ''
|
|
if is_teacher
|
|
# if the school owner is a teacher, show his bookings too
|
|
extra_teacher = " OR lesson_sessions.teacher_id = '#{user.teacher.id}'"
|
|
end
|
|
query = query.where('lesson_sessions.teacher_id in (?)' + extra_teacher, user.owned_school.teachers.map {|t| t.user.id})
|
|
query = query.where('lesson_sessions.status = ? OR lesson_sessions.status = ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED)
|
|
else
|
|
# this is a normal teacher (not a school owner)
|
|
if school_id && user.teacher.school.scheduling_comm?
|
|
# the school wants to control scheduling communication. So hide sessions in the requested/countered status from them
|
|
query = query.where('lesson_sessions.status != ? AND lesson_sessions.status != ?', LessonSession::STATUS_REQUESTED, LessonSession::STATUS_COUNTERED)
|
|
end
|
|
query = query.where('lesson_sessions.teacher_id = ?', user.id)
|
|
end
|
|
|
|
else
|
|
if user.school_id
|
|
school_extra = "OR (lesson_bookings.school_id = '#{user.school_id}')"
|
|
else
|
|
school_extra = ''
|
|
end
|
|
|
|
|
|
query = query.where('music_sessions.user_id = ?', user.id)
|
|
end
|
|
else
|
|
|
|
if user.school_id
|
|
school_extra = "OR (lesson_bookings.school_id = '#{user.school_id}')"
|
|
else
|
|
school_extra = ''
|
|
end
|
|
|
|
query = query.where('(lesson_sessions.teacher_id = ? or music_sessions.user_id = ?)', user.id, user.id)
|
|
end
|
|
|
|
query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?) ' + school_extra, user.id)
|
|
|
|
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
|
|
|
|
def school_owner_id
|
|
school = teacher.teacher.school
|
|
if school
|
|
school.owner.id
|
|
end
|
|
end
|
|
|
|
def access?(user)
|
|
user.id == music_session.user_id || user.id == teacher.id || user.id == school_owner_id
|
|
end
|
|
|
|
# teacher accepts the lesson
|
|
def accept(params)
|
|
response = self
|
|
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
|
|
message = '' if message.nil?
|
|
msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Approved")
|
|
Notification.send_jamclass_invitation_teacher(music_session, teacher)
|
|
Notification.send_student_jamclass_invitation(music_session, student)
|
|
Notification.send_lesson_message('accept', self, true)
|
|
|
|
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
|
|
message = '' if message.nil?
|
|
msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "All Lesson Times Updated")
|
|
Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept'
|
|
UserMailer.student_lesson_update_all(self, message, slot).deliver
|
|
UserMailer.teacher_lesson_update_all(self, message, slot).deliver
|
|
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
|
|
message = '' if message.nil?
|
|
msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "Lesson Updated Time Approved")
|
|
UserMailer.student_lesson_accepted(self, message, slot).deliver
|
|
UserMailer.teacher_lesson_accepted(self, message, slot).deliver
|
|
end
|
|
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.counterer = proposer
|
|
self.countered_at = Time.now
|
|
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 && !lesson_booking.counter(self, proposer, slot)
|
|
if !lesson_booking.counter(self, proposer, slot)
|
|
response = lesson_booking
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
else
|
|
response = self
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
|
|
message = '' if message.nil?
|
|
msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "New Time Proposed")
|
|
Notification.send_lesson_message('counter', self, slot.is_teacher_created?)
|
|
end
|
|
|
|
response
|
|
end
|
|
|
|
def cancel_lesson(canceler, message)
|
|
canceled_by_student = canceler == student
|
|
self.status = STATUS_CANCELED
|
|
self.cancel_message = message
|
|
self.canceler = canceler
|
|
self.canceling = true
|
|
|
|
if canceled_by_student
|
|
self.student_canceled = true
|
|
self.student_canceled_at = Time.now
|
|
self.student_canceled_reason = message
|
|
self.student_short_canceled = 24.hours.from_now > scheduled_start
|
|
else
|
|
self.teacher_canceled = true
|
|
self.teacher_canceled_at = Time.now
|
|
self.teacher_canceled_reason = message
|
|
self.teacher_short_canceled = 24.hours.from_now > scheduled_start
|
|
end
|
|
end
|
|
|
|
# canceled by the system because it is requested, and the end time has gone by
|
|
def autocancel
|
|
response = self
|
|
LessonSession.transaction do
|
|
self.autocanceling = true
|
|
self.status = LessonSession::STATUS_UNCONFIRMED
|
|
if self.save
|
|
if !lesson_booking.recurring
|
|
response = lesson_booking.autocancel
|
|
if response.errors.any?
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
end
|
|
else
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
|
|
msg = ChatMessage.create(teacher, nil, '', ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Timeout")
|
|
msg = ChatMessage.create(student, nil, '', ChatMessage::CHANNEL_LESSON, nil, teacher, self, "Lesson Timeout")
|
|
end
|
|
end
|
|
|
|
# teacher accepts the lesson
|
|
def cancel(params)
|
|
response = self
|
|
LessonSession.transaction do
|
|
|
|
self.status = LessonSession::STATUS_CANCELED
|
|
canceler = params[:canceler]
|
|
canceled_by_student = canceler == student
|
|
other = canceled_by_student ? teacher : student
|
|
message = params[:message]
|
|
message = '' if message.nil?
|
|
|
|
if lesson_booking.recurring
|
|
update_all = params[:update_all]
|
|
else
|
|
update_all = true
|
|
end
|
|
|
|
if lesson_booking.is_test_drive?
|
|
student.test_drive_declined(self)
|
|
end
|
|
|
|
if update_all
|
|
response = lesson_booking.cancel(canceler, other, message)
|
|
if response.errors.any?
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
|
|
if !save
|
|
response = self
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
else
|
|
cancel_lesson(canceler, message)
|
|
if !save
|
|
response = self
|
|
raise ActiveRecord::Rollback
|
|
end
|
|
|
|
msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, self, "Lesson Canceled")
|
|
Notification.send_lesson_message('canceled', self, false)
|
|
Notification.send_lesson_message('canceled', self, true)
|
|
UserMailer.student_lesson_canceled(self, message).deliver
|
|
UserMailer.teacher_lesson_canceled(self, message).deliver
|
|
end
|
|
end
|
|
|
|
response
|
|
end
|
|
|
|
def description(lesson_booking)
|
|
lesson_booking.lesson_package_type.description(lesson_booking)
|
|
end
|
|
|
|
def timed_description
|
|
if is_test_drive?
|
|
"TestDrive session with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
|
else
|
|
if self.lesson_booking.is_monthly_payment?
|
|
"Monthly Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
|
else
|
|
"Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def display_type
|
|
if is_test_drive?
|
|
'TestDrive'
|
|
elsif recurring
|
|
'Recurring'
|
|
else
|
|
'Single'
|
|
end
|
|
end
|
|
|
|
def last_student_comm_date
|
|
counterer_id == user_id ? countered_at : sent_notices_at
|
|
end
|
|
|
|
def days_no_response
|
|
date = last_student_comm_date
|
|
if date.nil?
|
|
'?'
|
|
else
|
|
(Time.now - last_student_comm_date) / (60 * 60 * 24)
|
|
end
|
|
|
|
|
|
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
|
|
|
|
def chat_url
|
|
APP_CONFIG.external_root_url + "/client#/jamclass/chat-dialog/d1=lesson_" + id
|
|
end
|
|
end
|
|
end
|