jam-cloud/ruby/lib/jam_ruby/models/lesson_session.rb

525 lines
16 KiB
Ruby

# represenst the type of lesson package
module JamRuby
class LessonSession < ActiveRecord::Base
attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson
@@log = Logging.logger[LessonSession]
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge
delegate :is_test_drive?, :is_single_free?, :is_normal?, to: :lesson_booking
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
STATUS_MISSED = 'missed'
STATUS_COMPLETED = 'completed'
STATUS_APPROVED = 'approved'
STATUS_SUSPENDED = 'suspended'
STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED, STATUS_APPROVED, STATUS_SUSPENDED]
LESSON_TYPE_SINGLE = 'paid'
LESSON_TYPE_SINGLE_FREE = 'single-free'
LESSON_TYPE_TEST_DRIVE = 'test-drive'
LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE]
has_one :music_session, class_name: "JamRuby::MusicSession"
belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id
belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id
has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution"
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
validates :duration, presence: true, numericality: {only_integer: true}
validates :lesson_booking, presence: true
validates :lesson_type, inclusion: {in: LESSON_TYPES}
validates :booked_price, presence: true
validates :status, presence: true, inclusion: {in: STATUS_TYPES}
validates :teacher_complete, inclusion: {in: [true, false]}
validates :student_complete, inclusion: {in: [true, false]}
validates :teacher_canceled, inclusion: {in: [true, false]}
validates :student_canceled, inclusion: {in: [true, false]}
validates :success, inclusion: {in: [true, false]}
validates :sent_notices, inclusion: {in: [true, false]}
validates :post_processed, inclusion: {in: [true, false]}
validate :validate_creating, :if => :creating
validate :validate_accepted, :if => :accepting
after_save :after_counter, :if => :countering
after_save :manage_slot_changes
after_create :create_charge
def create_charge
if !is_test_drive?
self.lesson_payment_charge = LessonPaymentCharge.new
lesson_payment_charge.amount_in_cents = 0
lesson_payment_charge.fee_in_cents = 0
lesson_payment_charge.lesson_session = self
lesson_payment_charge.save!
end
end
def manage_slot_changes
# if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted .
# TODO: what to do, what to do.
end
def suspend!
self.status = STATUS_SUSPENDED
self.save
end
def unsuspend!
self.status = STATUS_APPROVED
self.save
end
def self.hourly_check
analyse_sessions
complete_sessions
end
def self.analyse_sessions
MusicSession.joins(lesson_session: :lesson_booking).where('session_removed_at IS NOT NULL').where('analysed = false').each do |music_session|
lession_session = music_session.lesson_session
lession_session.analyse
end
end
def self.complete_sessions
MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('session_removed_at IS NOT NULL').where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session|
lession_session = music_session.lesson_session
lession_session.session_completed
end
end
def analyse
if self.analysed
return
end
analysis = LessonSessionAnalyser.analyse(self)
self.analysis = analysis_to_json(analysis)
self.success = analysis[:bill]
self.analysed_at = Time.now
self.analysed = true
if lesson_booking.requires_teacher_distribution?(self)
self.teacher_distribution = TeacherDistribution.create_for_lesson(self)
end
if self.save
# send out emails appropriate for this type of session
session_completed
end
end
def amount_charged
lesson_payment_charge.amount_in_cents / 100.0
end
def analysis_to_json(analysis)
json = {}
analysis.each do |k, v|
if v.is_a?(Array)
array = []
v.each do |item|
if item.is_a?(Range)
array << {begin: item.begin, end: item.end}
else
raise "expected range"
end
end
json[k] = array
else
json[k] = v
end
end
json.to_json
end
def session_completed
LessonSession.transaction do
self.lock!
if post_processed
# nothing to do. because this is an async job, it's possible this was queued up with another session_completed fired
return
end
if lesson_booking.is_test_drive?
test_drive_completed
elsif lesson_booking.is_normal?
if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment?
recurring_completed
else
normal_lesson_completed
end
end
end
end
def bill_lesson
lesson_payment_charge.charge
if lesson_payment_charge.billed
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
end
def test_drive_completed
distribution = teacher_distribution
if distribution # not all lessons/payment charges have a distribution
distribution.ready = true
distribution.save(validate: false)
end
if !sent_notices
if success
student.test_drive_succeeded(self)
else
student.test_drive_failed(self)
end
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
end
def recurring_completed
if success
if lesson_booking.is_monthly_payment?
# monthly payments are handled at beginning of month; just poke with email, and move on
if !sent_notices
# not in spec; just poke user and tell them we saw it was successfully completed
UserMailer.monthly_recurring_done(self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
else
bill_lesson
end
else
if lesson_booking.is_monthly_payment?
# bad session; just poke user
if !sent_notices
UserMailer.monthly_recurring_no_bill(self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
else
if !sent_notices
# bad session; just poke user
UserMailer.student_weekly_recurring_no_bill(student, self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
end
end
end
def normal_lesson_completed
if success
bill_lesson
else
if !sent_notices
UserMailer.student_lesson_normal_no_bill(self).deliver
UserMailer.teacher_lesson_no_bill(self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
end
end
def after_counter
send_counter(@countered_lesson, @countered_slot)
end
def scheduled_start
music_session.scheduled_start
end
def send_counter(countered_lesson, countered_slot)
if countered_slot.is_teacher_created?
UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver
else
UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver
end
self.countering = false
end
default_scope { order('lesson_sessions.created_at') }
def is_requested?
status == STATUS_REQUESTED
end
def is_canceled?
status == STATUS_CANCELED
end
def is_completed?
status == STATUS_COMPLETED
end
def is_missed?
status == STATUS_MISSED
end
def is_approved?
status == STATUS_APPROVED
end
def is_suspended?
status == STATUS_SUSPENDED
end
def 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 !is_requested?
self.errors.add(:status, "This session is already #{self.status}.")
end
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
self.accepting = false
self.status = STATUS_APPROVED
end
def self.create(booking)
lesson_session = LessonSession.new
lesson_session.creating = true
lesson_session.duration = booking.lesson_length
lesson_session.lesson_type = booking.lesson_type
lesson_session.lesson_booking = booking
lesson_session.booked_price = booking.booked_price
lesson_session.teacher = booking.teacher
lesson_session.status = booking.status
lesson_session.slot = booking.default_slot
if booking.is_test_drive?
lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase
end
lesson_session.save
if lesson_session.errors.any?
puts "Lesson Session errors #{lesson_session.errors.inspect}"
end
lesson_session
end
def student
music_session.creator
end
def self.index(user, params = {})
limit = params[:per_page]
limit ||= 100
limit = limit.to_i
query = LessonSession.joins(:music_session).joins(music_session: :creator)
query = query.includes([:teacher, :music_session])
query = query.order('music_sessions.scheduled_start DESC')
if params[:as_teacher]
query = query.where('lesson_sessions.teacher_id = ?', user.id)
else
query = query.where('music_sessions.user_id = ?', user.id)
end
current_page = params[:page].nil? ? 1 : params[:page].to_i
next_page = current_page + 1
# will_paginate gem
query = query.paginate(:page => current_page, :per_page => limit)
if query.length == 0 # no more results
{query: query, next_page: nil}
elsif query.length < limit # no more results
{query: query, next_page: nil}
else
{query: query, next_page: next_page}
end
end
def update_scheduled_start(week_offset)
music_session.scheduled_start = slot.scheduled_time(week_offset)
music_session.save
end
# grabs the next available time that's after the present, to avoid times being scheduled in the past
def update_next_available_time(attempt = 0)
max_attempts = attempt + 10
while attempt < max_attempts
test = slot.scheduled_time(attempt)
if test >= Time.now
time = test
# valid time found!
break
end
attempt += 1
end
if time
music_session.scheduled_start = time
music_session.save
end
time.nil? ? nil : attempt
end
# teacher accepts the lesson
def accept(params)
LessonSession.transaction do
message = params[:message]
slot = params[:slot]
slot = LessonBookingSlot.find(slot)
self.slot = slot
if is_approved?
# this implies a new slot has been countered, and now approved
if self.save
if slot.update_all
lesson_booking.accept(self, slot)
msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking)
Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept'
UserMailer.student_lesson_update_all(self, message, slot).deliver
UserMailer.teacher_lesson_update_all(self, message, slot).deliver
else
# nothing to do with the original booking (since we are not changing all times), so we update just ourself
time = update_next_available_time # XXX: week offset as 0? This *could* still be in the past. But the user is approving it. So do we just trust them and get out of their way?
if time.nil?
@@log.error("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past")
puts("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past")
raise ActiveRecord::Rollback
end
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}")
puts("unable to accept slot #{slot.id} for lesson #{self.id}")
end
else
self.accepting = true
if lesson_package_purchase.nil? && lesson_booking.is_test_drive?
self.lesson_package_purchase = student.most_recent_test_drive_purchase
end
if self.save
lesson_booking.accept(self, slot)
msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking)
Notification.send_lesson_message('accept', self, true)
UserMailer.student_lesson_accepted(self, message, slot).deliver
UserMailer.teacher_lesson_accepted(self, message, slot).deliver
else
@@log.error("unable to accept slot #{slot.id} for lesson #{self.id}")
puts("unable to accept slot #{slot.id} for lesson #{self.id}")
end
end
end
end
def counter(params)
proposer = params[:proposer]
slot = params[:slot]
message = params[:message]
self.countering = true
slot.proposer = proposer
slot.lesson_session = self
self.lesson_booking_slots << slot
self.countered_slot = slot
self.countered_lesson = self
if self.save
if slot.update_all || lesson_booking.is_requested?
lesson_booking.counter(self, proposer, slot)
end
else
raise ActiveRecord::Rollback
end
msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking)
Notification.send_lesson_message('counter', self, slot.is_teacher_created?)
end
def description(lesson_booking)
lesson_booking.lesson_package_type.description(lesson_booking)
end
def stripe_description(lesson_booking)
description(lesson_booking)
end
def home_url
APP_CONFIG.external_root_url + "/client#/jamclass"
end
def web_url
APP_CONFIG.external_root_url + "/client#/jamclass/lesson-request/" + id
end
def update_payment_url
APP_CONFIG.external_root_url + "/client#/jamclass/update-payment"
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + id
end
end
end