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

1233 lines
42 KiB
Ruby

# represenst the type of lesson package
module JamRuby
class LessonBooking < ActiveRecord::Base
include HtmlSanitize
html_sanitize strict: [:description, :cancel_message]
include ActiveModel::Dirty
@@log = Logging.logger[LessonBooking]
attr_accessor :accepting, :countering, :canceling, :autocanceling, :countered_slot, :countered_lesson, :current_purchase, :current_lesson, :expected_session_times, :adjustment_in_cents
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
STATUS_APPROVED = 'approved'
STATUS_SUSPENDED = 'suspended'
STATUS_COUNTERED = 'countered'
STATUS_COMPLETED = 'completed'
STATUS_UNCONFIRMED = 'unconfirmed'
STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED, STATUS_COMPLETED, STATUS_UNCONFIRMED]
LESSON_TYPE_FREE = 'single-free'
LESSON_TYPE_TEST_DRIVE = 'test-drive'
LESSON_TYPE_PAID = 'paid'
LESSON_TYPES = [LESSON_TYPE_FREE, LESSON_TYPE_TEST_DRIVE, LESSON_TYPE_PAID]
PAYMENT_STYLE_ELSEWHERE = 'elsewhere'
PAYMENT_STYLE_SINGLE = 'single'
PAYMENT_STYLE_WEEKLY = 'weekly'
PAYMENT_STYLE_MONTHLY = 'monthly'
ENGAGED = "status = '#{STATUS_APPROVED}' OR status = '#{STATUS_REQUESTED}' OR status = '#{STATUS_SUSPENDED}'"
PAYMENT_STYLES = [PAYMENT_STYLE_ELSEWHERE, PAYMENT_STYLE_SINGLE, PAYMENT_STYLE_WEEKLY, PAYMENT_STYLE_MONTHLY]
belongs_to :user, class_name: "JamRuby::User"
belongs_to :teacher, class_name: "JamRuby::User"
belongs_to :accepter, class_name: "JamRuby::User"
belongs_to :canceler, class_name: "JamRuby::User"
belongs_to :counterer, class_name: "JamRuby::User", foreign_key: :counterer_id
belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy
belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy
belongs_to :school, class_name: "JamRuby::School"
belongs_to :retailer, class_name: "JamRuby::Retailer"
belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice"
belongs_to :posa_card, class_name: "JamRuby::PosaCard"
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy
has_many :lesson_sessions, class_name: "JamRuby::LessonSession", :dependent => :destroy
has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase", :dependent => :destroy
validates :user, presence: true
validates :teacher, presence: true
validates :lesson_type, inclusion: {in: LESSON_TYPES}
validates :status, presence: true, inclusion: {in: STATUS_TYPES}
validates :recurring, inclusion: {in: [true, false]}
validates :sent_notices, inclusion: {in: [true, false]}
validates :card_presumed_ok, inclusion: {in: [true, false]}
validates :same_school, inclusion: {in: [true, false]}
validates :active, inclusion: {in: [true, false]}
validates :lesson_length, inclusion: {in: [30, 45, 60, 90, 120]}
validates :payment_style, inclusion: {in: PAYMENT_STYLES}
validates :booked_price, presence: true
validates :description, no_profanity: true, length: {minimum: 10, maximum: 20000}
validate :validate_user, on: :create
validate :validate_recurring
validate :validate_lesson_booking_slots
validate :validate_lesson_length
validate :validate_payment_style
validate :validate_uncollectables, on: :create
validate :validate_accepted, :if => :accepting
validate :validate_canceled, :if => :canceling
before_save :before_save
before_validation :before_validation
after_create :after_create
around_save :around_update
scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) }
scope :active, -> { where(active: true) }
scope :approved, -> { where(status: STATUS_APPROVED) }
scope :requested, -> { where(status: STATUS_REQUESTED) }
scope :canceled, -> { where(status: STATUS_CANCELED) }
scope :suspended, -> { where(status: STATUS_SUSPENDED) }
scope :engaged, -> { where(ENGAGED) }
scope :engaged_or_successful, -> { where("(" + ENGAGED + ") OR (lesson_bookings.status = '#{STATUS_COMPLETED}' AND lesson_bookings.success = true)") }
def before_validation
if self.booked_price.nil?
self.booked_price = compute_price
end
end
def after_create
if ((posa_card && posa_card.purchased) || card_presumed_ok || !payment_if_school_on_school?) && !sent_notices
send_notices
end
end
def before_save
automatically_default_slot
end
def around_update
@default_slot_did_change = self.default_slot_id_changed?
yield
sync_lessons
sync_remaining_test_drives
@default_slot_did_change = nil
@accepting = nil
@countering = nil
end
# here for shopping_cart
def product_info
if is_test_drive?
real_price = 0
elsif is_monthly_payment?
raise "no purchase assigned to lesson booking for monthly payment!" if current_purchase.nil?
real_price = self.current_purchase.teacher_distribution.jamkazam_margin
else
if current_lesson.nil?
puts "OHOHOMOOMG #{self.inspect}"
raise "no purchase assigned to lesson booking for lesson!"
end
real_price = self.current_lesson.teacher_distribution.jamkazam_margin
end
{price: real_price, real_price: real_price, total_price: real_price}
end
# here for shopping_cart
def price
booked_price
end
def is_countered?
has_recurring_counter?
end
def has_recurring_counter?
!!self.counter_slot && self.counter_slot.is_recurring?
end
def ever_accepted?
!!self.accepter
end
def no_slots
default_slot.from_package
end
def alt_slot
found = nil
lesson_booking_slots.each do |slot|
if slot.id != default_slot.id
found = slot
break
end
end
found
end
def student
user
end
def next_lesson
if recurring
session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first
if session.nil?
session = lesson_sessions[0]
end
LessonSession.find(session.id) if session
else
lesson_sessions[0]
end
end
def accept(lesson_session, slot, accepter)
if !is_active?
self.accepting = true
end
self.active = true
if slot.is_recurring?
if self.recurring
self.counter_slot = nil
self.status = STATUS_APPROVED
self.default_slot = slot
self.accepter = accepter
else
# should never happen because u shouldn't be able to set a recurring slot on a single lesson
end
else
self.status = STATUS_APPROVED
self.default_slot = slot
self.accepter = accepter
end
success = self.save
if !success
puts "unable to accept lesson booking #{errors.inspect}"
else
# ok, now we have to update the slots of our lesson_sessions
self.lesson_sessions.each do |lesson_session|
if !lesson_session.is_countered?
lesson_session.slot = slot
lesson_session.save
end
end
end
success
end
def counter(lesson_session, proposer, slot)
if slot.is_recurring?
self.lesson_booking_slots << slot
self.countering = true
self.counter_slot = slot
self.counterer = proposer
self.countered_at = Time.now
self.sent_counter_reminder = false
self.status = STATUS_COUNTERED
end
if self.default_slot.from_package
self.default_slot = slot
end
self.save
end
def automatically_default_slot
if is_requested? && default_slot.nil?
if lesson_booking_slots.length > 0
self.default_slot = lesson_booking_slots[0]
end
end
end
def sync_remaining_test_drives
if is_test_drive? || is_single_free?
if (posa_card || card_presumed_ok) && !user_decremented
self.user_decremented = true
self.save(validate: false)
if is_single_free?
user.remaining_free_lessons = user.remaining_free_lessons - 1
elsif is_test_drive?
if posa_card
user.jamclass_credits = user.jamclass_credits - 1
else
user.remaining_test_drives = user.remaining_test_drives - 1
end
end
user.save(validate: false)
end
end
end
def create_minimum_booking_time
# trying to be too smart
#(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60)
Time.now
end
def sync_lessons
if is_canceled? || is_completed?
# don't create new sessions if cancelled
return
end
if @default_slot_did_change
end
# Here we go; let's create a lesson(s) as needed
# we need to make lessons into the future a bit, to give time for everyone involved
minimum_start_time = create_minimum_booking_time
# get all sessions that are already scheduled for this booking ahead of the minimum time
sessions= MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").order(:created_at)
if recurring
# only want times ahead of this for recurring
sessions = sessions.where("scheduled_start > ?", minimum_start_time)
end
if @default_slot_did_change
# # adjust all session times
offset = 0
sessions.each_with_index do |item, i|
item.lesson_session.slot = default_slot
result = item.lesson_session.update_next_available_time(offset)
if result
offset = result
offset += 1
end
end
end
needed_sessions = determine_needed_sessions(sessions)
# if the latest scheduled session is after the minimum start time, then bump up minimum start time
last_session = sessions.last
last_session.reload if last_session # because of @default_slot_did_change logic above, this can be necessary
if last_session && last_session.scheduled_start && last_session.scheduled_start > minimum_start_time
minimum_start_time = last_session.scheduled_start
end
times = default_slot.scheduled_times(needed_sessions, minimum_start_time)
scheduled_lessons(times)
end
# sensitive to current time
def predicted_times_for_month(year, month)
first_day = Date.new(year, month, 1)
last_day = Date.new(year, month, -1)
sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start >= ?", first_day).where("scheduled_start <= ?", last_day).order(:created_at)
times = []
sessions.each do |session|
times << session.scheduled_start
end
last_session = sessions.last
start_day = first_day
if last_session
start_day = last_session.scheduled_start.to_date + 1
end
# now flesh out the rest of the month with predicted times
more_times = default_slot.scheduled_times(5, start_day)
more_times.each do |time|
if time.to_date >= first_day && time.to_date <= last_day
times << time
end
end
{times: times, session: sessions.first}
end
def determine_needed_sessions(sessions)
needed_sessions = 0
if is_requested?
# in the case of a requested booking (not approved) only make one, even if it's recurring. This is for UI considerations
if sessions.count == 0
needed_sessions = 1
end
elsif is_active?
expected_num_sessions = recurring ? 2 : 1
needed_sessions = expected_num_sessions - sessions.count
end
needed_sessions
end
def scheduled_lessons(times)
times.each do |time|
lesson_session = LessonSession.create(self)
if lesson_session.errors.any?
puts "JamClass lesson session creation errors #{lesson_session.errors.inspect}"
@@log.error("JamClass lesson session creation errors #{lesson_session.errors.inspect}")
raise ActiveRecord::Rollback
end
ms_tz = ActiveSupport::TimeZone.new(default_slot.timezone)
ms_tz = "#{ms_tz.name},#{default_slot.timezone}"
rsvps = [{instrument_id: 'other', proficiency_level: 0, approve: true}]
music_session = MusicSession.create(student, {
name: "#{display_type2} JamClass taught by #{teacher.name}",
description: "This is a #{lesson_length}-minute #{display_type2} lesson with #{teacher.name}.",
musician_access: false,
fan_access: false,
genres: ['other'],
approval_required: false,
fan_chat: false,
legal_policy: "standard",
language: 'eng',
duration: lesson_length,
recurring_mode: false,
timezone: ms_tz,
create_type: MusicSession::CREATE_TYPE_LESSON,
is_unstructured_rsvp: true,
scheduled_start: time,
invitations: [teacher.id],
lesson_session: lesson_session,
rsvp_slots: rsvps
})
if music_session.errors.any?
puts "JamClass lesson scheduling errors #{music_session.errors.inspect}"
@@log.error("JamClass lesson scheduling errors #{music_session.errors.inspect}")
raise ActiveRecord::Rollback
end
if lesson_session.is_active?
# send out email to student to act as something they can add to their calendar
Notification.send_student_jamclass_invitation(music_session, student)
end
end
end
def is_weekly_payment?
payment_style == PAYMENT_STYLE_WEEKLY
end
def is_monthly_payment?
payment_style == PAYMENT_STYLE_MONTHLY
end
def requires_per_session_billing?
is_normal? && !is_monthly_payment?
end
def requires_teacher_distribution?(target)
if 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)
is_monthly_payment?
else
raise "unable to determine object type of #{target}"
end
end
def is_requested?
status == STATUS_REQUESTED
end
def is_canceled?
status == STATUS_CANCELED
end
def is_completed?
status == STATUS_COMPLETED
end
def is_approved?
status == STATUS_APPROVED
end
def is_suspended?
status == STATUS_SUSPENDED
end
def is_active?
active
end
def has_access?(user)
user.id == student.id || user.id == teacher.id || (self.school.nil? ? false : self.school.user.id == user.id)
end
def validate_accepted
# accept is multipe purpose; either accept the initial request, or a counter slot
if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED
self.errors.add(:status, "This lesson is already #{self.status}.")
end
if self.accepter.nil?
self.errors.add(:accepter, "No one has been indicated as accepting the lesson")
end
self.accepting = false
end
def validate_canceled
if !is_canceled?
self.errors.add(:status, "This session is already #{self.status}.")
end
self.canceling = false
end
def send_notices
UserMailer.student_lesson_request(self).deliver_now
UserMailer.teacher_lesson_request(self).deliver_now
Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept'
self.sent_notices = true
self.sent_notices_at = Time.now
self.save
end
def resolved_test_drive_package
result = nil
# posa card is best indicator of lesson package type
if posa_card
return posa_card.lesson_package_type
end
purchase = student.most_recent_test_drive_purchase
if purchase
# for lessons already packaged
result = purchase.lesson_package_type
else
# for unbooked lessons
result = student.desired_package
end
if result.nil?
result = LessonPackageType.test_drive_4
end
result
end
def lesson_package_type
if is_single_free?
LessonPackageType.single_free
elsif is_test_drive?
resolved_test_drive_package
elsif is_normal?
LessonPackageType.single
end
end
def display_type2
if is_single_free?
"Free"
elsif is_test_drive?
"TestDrive"
elsif is_normal?
"Single"
end
end
def display_type
if is_single_free?
"Free"
elsif is_test_drive?
"TestDrive"
elsif is_normal?
if recurring
"recurring"
else
"single"
end
end
end
# determine the price of this booking based on what the user wants, and the teacher's pricing
def compute_price
if is_single_free?
0
elsif is_test_drive?
resolved_test_drive_package.price
elsif is_normal?
teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY)
end
end
def distribution_price_in_cents(target, education, split = nil)
if split
distribution = teacher_distribution_price_in_cents(target, split)
(distribution * split).round
elsif education
distribution = teacher_distribution_price_in_cents(target, 0.0625)
(distribution * 0.0625).round # 0.0625 is 1/4th of 25%
else
distribution = teacher_distribution_price_in_cents(target)
distribution
end
end
def teacher_distribution_price_in_cents(target, split = nil)
if is_single_free?
0
elsif is_test_drive?
10 * 100
elsif is_normal?
if is_monthly_payment?
raise "not a LessonPackagePurchase: #{target.inspect}" if !target.is_a?(LessonPackagePurchase)
today = Date.today
start_date = Date.new(target.year, target.month, 1)
if today.year == target.year && today.month == target.month
# we are in the month being billed. we should set the start date based on today
start_date = today
end
price, times = LessonSessionMonthlyPrice.price(self, start_date)
price_in_cents = (price * 100).round
# OK, we have a suggested price based on date, but we need to now adjust if previous lessons have been unsuccessful
adjusted_price_in_cents = LessonSessionMonthlyPrice.adjust_for_missed_lessons(self, price_in_cents, split)
self.expected_session_times = times # save for later
self.adjustment_in_cents = price_in_cents - adjusted_price_in_cents
adjusted_price_in_cents
else
booked_price * 100
end
end
end
# find any lesson package purchases for this lesson booking for previous months that have not had adjustments
# we have to base this on 'now', meaning we do not consider months until they are fully closed
# this does mean, because we collect up to a week in advance of a new month starting, that a student will likely not see adjustments until 2 cycles forward
def self.previous_needing_adjustment
now = Time.now.utc
year = now.year
month = now.month
if month == 1
previous_year = year - 1
previous_month = 12
else
previous_year = year
previous_month = month - 1
end
LessonPackagePurchase.where(recurring: true).where('month <= ?', previous_month).where('year <= ?', previous_year).where('actual_session_times is null').where('expected_session_times is not null')
.limit(500)
end
def self.adjust_for_missed_sessions
# Go to previous lesson_package_purchase month, and see if we need to adjust for a roll forward due to missed sessions
previous_purchases = LessonBooking.previous_needing_adjustment
previous_purchases.each do |previous_purchase|
# XXX other monthly code uses session start time. should we be doing that?
successful_lessons = LessonSession.where(lesson_booking: previous_purchase.lesson_booking).where(success: true).where('analysed_at >= ? AND analysed_at < ?', previous_purchase.beginning_of_month_at, previous_purchase.end_of_month_at)
previous_purchase.actual_session_times = successful_lessons.count
# find out how many actual lessons were had, and then we can adjust price of this current distribution (amount_in_cents) accordingly
ratio = previous_purchase.actual_session_times.to_f / previous_purchase.expected_session_times.to_f
if ratio < 1
# discount next month for student
amount_paid_last_month_in_cents = previous_purchase.price_in_cents # this does not include tax. It's just the expected price of the booking
previous_purchase.remaining_roll_forward_amount_in_cents = previous_purchase.total_roll_forward_amount_in_cents = (amount_paid_last_month_in_cents * ratio).round
# if there is a roll forward, add it to the lesson booking
previous_purchase.lesson_booking.remaining_roll_forward_amount_in_cents += previous_purchase.remaining_roll_forward_amount_in_cents
previous_purchase.lesson_booking.save!
else
previous_purchase.total_roll_forward_amount_in_cents = 0
previous_purchase.applied_roll_forward_amount_in_cents = 0
end
previous_purchase.save
end
end
def is_single_free?
lesson_type == LESSON_TYPE_FREE
end
def is_test_drive?
lesson_type == LESSON_TYPE_TEST_DRIVE
end
def is_normal?
lesson_type == LESSON_TYPE_PAID
end
def dayWeekDesc(slot = default_slot)
day = case slot.day_of_week
when 0 then
"Sunday"
when 1 then
"Monday"
when 2 then
"Tuesday"
when 3 then
"Wednesday"
when 4 then
"Thursday"
when 5 then
"Friday"
when 6 then
"Saturday"
end
if slot.hour > 11
hour = slot.hour - 12
if hour == 0
hour = 12
end
am_pm = 'pm'
else
hour = slot.hour
if hour == 0
hour = 12
end
am_pm = 'am'
end
"#{day} at #{hour}:#{slot.minute.to_s.rjust(2, "0")}#{am_pm}"
end
def status_as_verb
if is_requested?
'requested'
else
'scheduled'
end
end
def approved_before?
!self.accepter_id.nil?
end
def autocancel
self.autocanceling = true
self.active = false
self.status = STATUS_UNCONFIRMED
save
self
end
def cancel_tracking(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
else
self.teacher_canceled = true
self.teacher_canceled_at = Time.now
self.teacher_canceled_reason = message
end
end
def cancel(canceler, other, message)
cancel_tracking(canceler, message)
self.active = false
success = save
if success
lesson_sessions.upcoming.each do |lesson_session|
lesson_session = LessonSession.find(lesson_session.id) # because .upcoming creates ReadOnly records
lesson_session.cancel_lesson(canceler, message)
if !lesson_session.save
return lesson_session
end
end
if approved_before?
# just tell both people it's cancelled, to act as confirmation
Notification.send_lesson_message('canceled', next_lesson, false)
Notification.send_lesson_message('canceled', next_lesson, true)
UserMailer.student_lesson_booking_canceled(self, message).deliver_now
UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now
purpose = "Lesson Canceled"
else
if canceler == student
# if it's the first time acceptance student canceling, we call it a 'cancel'
Notification.send_lesson_message('canceled', next_lesson, false)
UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now
purpose = "Lesson Canceled"
else
# if it's the first time acceptance teacher, it was declined
UserMailer.student_lesson_booking_declined(self, message).deliver_now
Notification.send_lesson_message('declined', next_lesson, true)
purpose = "Lesson Declined"
end
end
message = '' if message.nil?
msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, next_lesson, purpose)
else
end
self
end
def card_approved
self.card_presumed_ok = true
if posa_card_id
self.posa_card_purchased = true
end
if self.save && !sent_notices
send_notices
end
end
def validate_user
if card_presumed_ok && is_single_free?
if !user.has_free_lessons?
errors.add(:user, 'have no remaining free lessons')
end
#if !user.has_stored_credit_card?
# errors.add(:user, 'has no credit card stored')
#end
elsif is_test_drive?
if posa_card
if !user.has_posa_credits?
errors.add(:user, "have no remaining jamclass credits")
end
else
if !user.has_test_drives? && !user.can_buy_test_drive?
errors.add(:user, "have no remaining test drives")
elsif teacher.has_booked_test_drive_with_student?(user) && !user.admin
errors.add(:user, "have an in-progress or successful TestDrive with this teacher already")
end
end
elsif is_normal?
#if !user.has_stored_credit_card?
# errors.add(:user, 'has no credit card stored')
#end
end
end
def validate_teacher
# shouldn't we check if the teacher already has a booking in this time slot, or at least warn the user
end
def validate_recurring
if is_single_free? || is_test_drive?
if recurring
errors.add(:recurring, "can not be true for this type of lesson")
end
end
false
end
def validate_lesson_booking_slots
if test_drive_package_choice.nil?
if lesson_booking_slots.length == 0 || lesson_booking_slots.length == 1
errors.add(:lesson_booking_slots, "must have two times specified")
end
end
end
def validate_lesson_length
if is_single_free? || is_test_drive?
if lesson_length != 30
errors.add(:lesson_length, "must be 30 minutes")
end
end
end
def validate_payment_style
if is_normal?
if payment_style.nil?
errors.add(:payment_style, "can't be blank")
end
end
end
def validate_uncollectables
if user.uncollectables.count > 0
errors.add(:user, 'have unpaid lessons.')
end
end
def school_owned?
!!school
end
def self.book_packaged_test_drive(user, teacher, description, test_drive_package_choice)
book_test_drive(user, teacher, LessonBookingSlot.packaged_slots, description, test_drive_package_choice)
end
def self.book_free(user, teacher, lesson_booking_slots, description)
self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description)
end
def self.book_test_drive(user, teacher, lesson_booking_slots, description, test_drive_package_choice = nil)
self.book(user, teacher, LessonBooking::LESSON_TYPE_TEST_DRIVE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description, test_drive_package_choice)
end
def self.book_normal(user, teacher, lesson_booking_slots, description, recurring, payment_style, lesson_length)
self.book(user, teacher, LessonBooking::LESSON_TYPE_PAID, lesson_booking_slots, recurring, lesson_length, payment_style, description)
end
def self.book(user, teacher, lesson_type, lesson_booking_slots, recurring, lesson_length, payment_style, description, test_drive_package_choice = nil)
lesson_booking = nil
LessonBooking.transaction do
lesson_booking = LessonBooking.new
lesson_booking.user = user
lesson_booking.card_presumed_ok = user.has_stored_credit_card?
lesson_booking.sent_notices = false
lesson_booking.teacher = teacher
lesson_booking.lesson_type = lesson_type
lesson_booking.recurring = recurring
lesson_booking.lesson_length = lesson_length
lesson_booking.payment_style = payment_style
lesson_booking.description = description
lesson_booking.status = STATUS_REQUESTED
if lesson_type == LESSON_TYPE_TEST_DRIVE
# if the user has any jamclass credits, then we should get their most recent posa purchase
if user.jamclass_credits > 0
lesson_booking.posa_card = user.most_recent_posa_card
if lesson_booking.posa_card
lesson_booking.posa_card_purchased = lesson_booking.posa_card.purchased
end
else
# otherwise, it's a normal test drive, and we should honor test_drive_package_choice if specified
lesson_booking.test_drive_package_choice = test_drive_package_choice
end
end
if lesson_booking.teacher && lesson_booking.teacher.teacher.school
lesson_booking.school = lesson_booking.teacher.teacher.school
end
# copy payment settings from retailer into lesson booking
if lesson_booking.teacher && lesson_booking.teacher.teacher.retailer
lesson_booking.retailer = lesson_booking.teacher.teacher.retailer
lesson_booking.payment = lesson_booking.teacher.teacher.retailer.payment_details.to_json
lesson_booking.same_retailer = lesson_booking.teacher.teacher.retailer.affiliate_partner == user.affiliate_referral
end
if user
lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id))
if lesson_booking.same_school
lesson_booking.same_school_free = false # !user.school.education # non-education schools (music schools) are 'free' when school-on-school
end
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
lesson_booking.lesson_booking_slots = lesson_booking_slots
lesson_booking_slots.each do |slot|
slot.lesson_booking = lesson_booking
slot.message = description
end if lesson_booking_slots
if lesson_booking.save
description = '' if description.nil?
msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking.lesson_sessions[0], 'Lesson Requested')
end
end
lesson_booking
end
def self.unprocessed(current_user)
LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where(same_school_free: false).where('posa_card_id is null OR (posa_card_id is not null AND posa_card_purchased = false)')
end
def self.requested(current_user)
LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED)
end
def school_on_school?
same_school
end
def school_on_school_payment?
#!!(same_school && (school.education || school.is_guitar_center?))
same_school
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]
else
[teacher.email]
end
end
def school_and_teacher_ids
if school && school.scheduling_comm?
[school.owner.id, teacher.id]
else
[teacher.id]
end
end
def school_over_teacher
if school && school.scheduling_comm?
[school.communication_email]
else
[teacher.email]
end
end
def school_over_teacher_ids
if school && school.scheduling_comm?
[school.owner.id]
else
[teacher.id]
end
end
def self.find_bookings_needing_sessions(minimum_start_time)
MusicSession.select([:lesson_booking_id]).joins(:lesson_session => :lesson_booking).where("lesson_bookings.active = true").where('lesson_bookings.recurring = true').where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).group(:lesson_booking_id).having('count(lesson_booking_id) < 2')
end
# check for any recurring sessions where there are not at least 2 sessions into the future. If not, we need to make sure they get made
def self.hourly_check
schedule_upcoming_lessons
# order matters: bill_monthly code will use the adjustments made in here to correct billing roll forward
adjust_for_missed_sessions
# needs to come after 'adjust_for_missed_sessions'
bill_monthlies
end
def self.bill_monthlies
now = Time.now
billable_monthlies(now).each do |lesson_booking|
lesson_booking.bill_monthly(now)
end
today = now.to_date
seven_days_in_future = today + 7
is_different_month = seven_days_in_future.month != today.month
if is_different_month
next_month = seven_days_in_future.to_time
billable_monthlies(next_month).each do |lesson_booking|
lesson_booking.bill_monthly(next_month)
end
end
end
def self.billable_monthlies(now)
current_month_first_day = Date.new(now.year, now.month, 1)
current_month_last_day = Date.new(now.year, now.month, -1)
#next_month_last_day = now.month == 12 ? Date.new(now.year + 1, 1, -1) : Date.new(now.year, now.month + 1, -1)
LessonBooking
.joins(:lesson_sessions => :music_session)
.joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND (lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}))")
.where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)")
.where(payment_style: PAYMENT_STYLE_MONTHLY)
.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
=begin
today = now.to_date
seven_days_in_future = today + 7
is_different_month = seven_days_in_future.month != today.month
if is_different_month
condition = "(((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) )
OR ((lesson_package_purchases.year = #{seven_days_in_future.year} AND lesson_package_purchases.month = #{seven_days_in_future.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{seven_days_in_future.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{seven_days_in_future.month} ) ) )"
else
condition = "((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) )"
end
# .where("(lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) OR (lesson_package_purchases.year = #{next_month_last_day.year} AND lesson_package_purchases.month = #{next_month_last_day.month})")
# find any monthly-billed bookings that have a session coming up within 7 days, and if so, attempt to bill them
LessonBooking
.joins(:lesson_sessions)
.joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND #{condition})")
.where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)")
.where(payment_style: PAYMENT_STYLE_MONTHLY)
.where(status: STATUS_APPROVED)
.where('lesson_sessions.created_at >= ?', current_month_first_day)
.where('lesson_sessions.created_at <= ?', seven_days_in_future).uniq
=end
end
def self.bookings(student, teacher, since_at = nil)
bookings = LessonBooking.where(user_id: student.id, teacher_id: teacher.id)
if since_at
bookings = bookings.where('created_at >= ?', since_at)
end
bookings
end
def self.not_failed
end
def self.engaged_bookings(student, teacher, since_at = nil)
bookings = bookings(student, teacher, since_at)
bookings.engaged_or_successful
end
def bill_monthly(now)
LessonBooking.transaction do
self.lock!
current_month = Date.new(now.year, now.month, 1)
bill_for_month(current_month)
today = now.to_date
seven_days_in_future = today + 7
is_different_month = seven_days_in_future.month != today.month
if is_different_month
bill_for_month(seven_days_in_future)
end
end
end
def bill_for_month(day_in_month)
# try to find lesson package purchase for this month, and last month, and see if they need processing
current_month_purchase = lesson_package_purchases.where(lesson_booking_id: self.id, user_id: student.id, year: day_in_month.year, month: day_in_month.month).first
if current_month_purchase.nil?
current_month_purchase = LessonPackagePurchase.create(user, self, lesson_package_type, day_in_month.year, day_in_month.month)
end
current_month_purchase.bill_monthly
end
def suspend!
# when this is called, the calling code sends out a email to let the student and teacher know (it feels unnatural it's not here, though)
self.status = STATUS_SUSPENDED
self.active = false
if self.save
future_sessions.each do |lesson_session|
LessonSession.find(lesson_session.id).suspend!
end
end
end
def unsuspend!
if self.status == STATUS_SUSPENDED
self.status = STATUS_APPROVED
self.active = true
if self.save
future_sessions.each do |lesson_session|
LessonSession.find(lesson_session.id).unsuspend!
end
end
end
end
def future_sessions
lesson_sessions.joins(:music_session).where('scheduled_start > ?', Time.now).order(:created_at)
end
def self.schedule_upcoming_lessons
minimum_start_time = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60)
lesson_bookings = find_bookings_needing_sessions(minimum_start_time)
lesson_bookings.each do |data|
lesson_booking = LessonBooking.find(data["lesson_booking_id"])
lesson_booking.sync_lessons
end
end
def scheduling_email
school_scheduling_comm? ? school.communication_email : teacher.email
end
# when you need to email potentially both school and teacher for same email
def teacher_school_emails
if school_comm?
[school.communication_email, teacher.email]
else
[teacher.email]
end
end
def home_url
APP_CONFIG.external_root_url + "/client#/jamclass"
end
def web_url
APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id
end
def update_payment_url
APP_CONFIG.external_root_url + "/client#/jamclass/update-payment"
end
def admin_url
APP_CONFIG.admin_root_url + "/admin/lesson_bookings/" + id
end
private
def school_scheduling_comm?
school ? school.school_comm? : false
end
end
end