2016-04-06 02:23:15 +00:00
# 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 ]
2017-07-10 02:21:29 +00:00
attr_accessor :accepting , :countering , :canceling , :autocanceling , :countered_slot , :countered_lesson , :current_purchase , :current_lesson , :expected_session_times , :adjustment_in_cents
2016-04-06 02:23:15 +00:00
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
STATUS_APPROVED = 'approved'
STATUS_SUSPENDED = 'suspended'
STATUS_COUNTERED = 'countered'
2016-05-20 02:42:27 +00:00
STATUS_COMPLETED = 'completed'
2016-05-28 23:48:40 +00:00
STATUS_UNCONFIRMED = 'unconfirmed'
2016-04-06 02:23:15 +00:00
2016-05-28 23:48:40 +00:00
STATUS_TYPES = [ STATUS_REQUESTED , STATUS_CANCELED , STATUS_APPROVED , STATUS_SUSPENDED , STATUS_COUNTERED , STATUS_COMPLETED , STATUS_UNCONFIRMED ]
2016-04-06 02:23:15 +00:00
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'
2016-05-20 02:42:27 +00:00
ENGAGED = " status = ' #{ STATUS_APPROVED } ' OR status = ' #{ STATUS_REQUESTED } ' OR status = ' #{ STATUS_SUSPENDED } ' "
2016-04-06 02:23:15 +00:00
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 "
2016-05-28 23:48:40 +00:00
belongs_to :counterer , class_name : " JamRuby::User " , foreign_key : :counterer_id
2016-04-21 14:23:29 +00:00
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
2016-05-12 21:29:27 +00:00
belongs_to :school , class_name : " JamRuby::School "
2017-01-17 18:24:49 +00:00
belongs_to :retailer , class_name : " JamRuby::Retailer "
2016-06-03 04:32:09 +00:00
belongs_to :test_drive_package_choice , class_name : " JamRuby::TestDrivePackageChoice "
2016-10-03 02:51:34 +00:00
belongs_to :posa_card , class_name : " JamRuby::PosaCard "
2016-04-26 11:58:14 +00:00
has_many :lesson_booking_slots , class_name : " JamRuby::LessonBookingSlot " , :dependent = > :destroy
2016-04-21 14:23:29 +00:00
has_many :lesson_sessions , class_name : " JamRuby::LessonSession " , :dependent = > :destroy
has_many :lesson_package_purchases , class_name : " JamRuby::LessonPackagePurchase " , :dependent = > :destroy
2016-04-06 02:23:15 +00:00
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 ] }
2016-05-16 16:39:20 +00:00
validates :same_school , inclusion : { in : [ true , false ] }
2016-04-06 02:23:15 +00:00
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
2016-05-12 21:29:27 +00:00
validate :validate_uncollectables , on : :create
2016-04-06 02:23:15 +00:00
validate :validate_accepted , :if = > :accepting
2016-04-21 14:23:29 +00:00
validate :validate_canceled , :if = > :canceling
2016-04-06 02:23:15 +00:00
before_save :before_save
before_validation :before_validation
after_create :after_create
around_save :around_update
2016-10-03 02:51:34 +00:00
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) " ) }
2016-04-06 02:23:15 +00:00
def before_validation
if self . booked_price . nil?
self . booked_price = compute_price
end
end
def after_create
2017-07-10 02:21:29 +00:00
if ( ( posa_card && posa_card . purchased ) || card_presumed_ok || ! payment_if_school_on_school? ) && ! sent_notices
2016-04-06 02:23:15 +00:00
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
2016-05-16 16:39:20 +00:00
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 } "
2016-10-03 02:51:34 +00:00
raise " no purchase assigned to lesson booking for lesson! "
2016-05-16 16:39:20 +00:00
end
real_price = self . current_lesson . teacher_distribution . jamkazam_margin
end
{ price : real_price , real_price : real_price , total_price : real_price }
2016-04-06 02:23:15 +00:00
end
2016-10-03 02:51:34 +00:00
2016-04-06 02:23:15 +00:00
# here for shopping_cart
def price
booked_price
end
2017-07-17 00:38:40 +00:00
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
2016-06-03 04:32:09 +00:00
def no_slots
default_slot . from_package
end
2016-04-06 02:23:15 +00:00
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
2016-04-21 14:23:29 +00:00
if session . nil?
session = lesson_sessions [ 0 ]
end
2016-04-06 02:23:15 +00:00
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
2017-07-17 00:38:40 +00:00
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
2016-04-06 02:23:15 +00:00
success = self . save
if ! success
2017-07-17 00:38:40 +00:00
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
2016-04-06 02:23:15 +00:00
success
end
def counter ( lesson_session , proposer , slot )
2017-07-17 00:38:40 +00:00
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
2016-06-03 04:32:09 +00:00
if self . default_slot . from_package
self . default_slot = slot
end
2016-04-06 02:23:15 +00:00
self . save
end
def automatically_default_slot
2016-06-03 04:32:09 +00:00
if is_requested? && default_slot . nil?
2016-04-06 02:23:15 +00:00
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?
2016-10-03 02:51:34 +00:00
if ( posa_card || card_presumed_ok ) && ! user_decremented
2016-04-06 02:23:15 +00:00
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?
2016-09-27 02:56:12 +00:00
if posa_card
user . jamclass_credits = user . jamclass_credits - 1
else
user . remaining_test_drives = user . remaining_test_drives - 1
end
2016-04-06 02:23:15 +00:00
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
2016-05-20 02:42:27 +00:00
if is_canceled? || is_completed?
2016-04-06 02:23:15 +00:00
# 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
2016-05-30 03:30:33 +00:00
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
2016-04-06 02:23:15 +00:00
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
2016-10-03 02:51:34 +00:00
{ times : times , session : sessions . first }
2016-04-06 02:23:15 +00:00
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 )
2016-09-08 10:59:58 +00:00
if no_school_on_school_payment?
return false
2016-05-16 16:39:20 +00:00
elsif target . is_a? ( JamRuby :: LessonSession )
2016-04-06 02:23:15 +00:00
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
2016-05-20 02:42:27 +00:00
def is_completed?
status == STATUS_COMPLETED
end
2016-04-06 02:23:15 +00:00
def is_approved?
status == STATUS_APPROVED
end
def is_suspended?
status == STATUS_SUSPENDED
end
def is_active?
active
end
2017-10-18 02:36:53 +00:00
def has_access? ( user )
user . id == student . id || user . id == teacher . id || ( self . school . nil? ? false : self . school . user . id == user . id )
end
2016-04-06 02:23:15 +00:00
def validate_accepted
# accept is multipe purpose; either accept the initial request, or a counter slot
2017-07-17 00:38:40 +00:00
if self . status_was != STATUS_REQUESTED && self . status_was != STATUS_COUNTERED
2016-04-06 02:23:15 +00:00
self . errors . add ( :status , " This lesson is already #{ self . status } . " )
end
2016-05-30 21:43:55 +00:00
if self . accepter . nil?
self . errors . add ( :accepter , " No one has been indicated as accepting the lesson " )
end
2016-04-06 02:23:15 +00:00
self . accepting = false
end
2016-04-21 14:23:29 +00:00
def validate_canceled
if ! is_canceled?
self . errors . add ( :status , " This session is already #{ self . status } . " )
end
self . canceling = false
end
2016-04-06 02:23:15 +00:00
def send_notices
2016-07-17 15:16:27 +00:00
UserMailer . student_lesson_request ( self ) . deliver_now
UserMailer . teacher_lesson_request ( self ) . deliver_now
2016-04-06 02:23:15 +00:00
Notification . send_lesson_message ( 'requested' , lesson_sessions [ 0 ] , false ) # TODO: this isn't quite an 'accept'
self . sent_notices = true
2016-05-28 23:48:40 +00:00
self . sent_notices_at = Time . now
2016-04-06 02:23:15 +00:00
self . save
end
2016-05-05 02:20:38 +00:00
def resolved_test_drive_package
result = nil
2017-07-10 02:21:29 +00:00
# posa card is best indicator of lesson package type
if posa_card
return posa_card . lesson_package_type
end
2016-05-05 02:20:38 +00:00
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
2016-05-29 16:17:26 +00:00
2016-04-06 02:23:15 +00:00
def lesson_package_type
if is_single_free?
LessonPackageType . single_free
elsif is_test_drive?
2016-05-05 02:20:38 +00:00
resolved_test_drive_package
2016-04-06 02:23:15 +00:00
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?
2016-05-05 02:20:38 +00:00
resolved_test_drive_package . price
2016-04-06 02:23:15 +00:00
elsif is_normal?
teacher . teacher . booking_price ( lesson_length , payment_style != PAYMENT_STYLE_MONTHLY )
end
end
2017-07-10 02:21:29 +00:00
def distribution_price_in_cents ( target , education , split = nil )
2016-09-08 10:59:58 +00:00
2017-01-17 18:24:49 +00:00
if split
2017-07-10 02:21:29 +00:00
distribution = teacher_distribution_price_in_cents ( target , split )
2017-01-17 18:24:49 +00:00
( distribution * split ) . round
elsif education
2017-07-10 02:21:29 +00:00
distribution = teacher_distribution_price_in_cents ( target , 0 . 0625 )
2016-12-15 18:47:08 +00:00
( distribution * 0 . 0625 ) . round # 0.0625 is 1/4th of 25%
2016-09-08 10:59:58 +00:00
else
2017-07-10 02:21:29 +00:00
distribution = teacher_distribution_price_in_cents ( target )
2016-09-08 10:59:58 +00:00
distribution
end
end
2017-07-10 02:21:29 +00:00
def teacher_distribution_price_in_cents ( target , split = nil )
2016-04-06 02:23:15 +00:00
if is_single_free?
0
elsif is_test_drive?
10 * 100
elsif is_normal?
2016-04-26 17:36:06 +00:00
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
2017-07-10 02:21:29 +00:00
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
2016-04-26 17:36:06 +00:00
else
booked_price * 100
end
2016-04-06 02:23:15 +00:00
end
end
2017-07-10 02:21:29 +00:00
# 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
2018-06-03 19:11:29 +00:00
#previous_purchase.applied_roll_forward_amount_in_cents = 0
2017-07-10 02:21:29 +00:00
end
previous_purchase . save
end
end
2016-04-06 02:23:15 +00:00
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
2016-10-03 02:51:34 +00:00
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
2016-04-06 02:23:15 +00:00
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
2017-07-17 00:38:40 +00:00
" #{ day } at #{ hour } : #{ slot . minute . to_s . rjust ( 2 , " 0 " ) } #{ am_pm } "
2016-04-06 02:23:15 +00:00
end
2017-07-17 00:38:40 +00:00
def status_as_verb
if is_requested?
'requested'
else
'scheduled'
end
end
2016-04-06 02:23:15 +00:00
def approved_before?
! self . accepter_id . nil?
end
2016-04-21 14:23:29 +00:00
2016-05-28 23:48:40 +00:00
def autocancel
self . autocanceling = true
self . active = false
self . status = STATUS_UNCONFIRMED
2018-03-03 21:39:55 +00:00
if save
if is_test_drive?
user . jamclass_credits = user . jamclass_credits + 1
user . save ( validate : false )
end
end
2016-05-28 23:48:40 +00:00
self
end
2016-10-03 02:51:34 +00:00
2018-01-27 22:18:04 +00:00
def cancel_tracking ( canceler , message , canceled_by_admin = false )
2017-07-17 00:38:40 +00:00
canceled_by_student = canceler == student
2016-04-06 02:23:15 +00:00
self . status = STATUS_CANCELED
self . cancel_message = message
2018-01-27 22:18:04 +00:00
self . canceler = canceler if ! canceled_by_admin
2017-07-17 00:38:40 +00:00
self . canceling = true
2018-01-27 22:18:04 +00:00
if canceled_by_admin
self . canceled_by_admin = Time . now
elsif canceled_by_student
2017-07-17 00:38:40 +00:00
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
2018-01-27 22:18:04 +00:00
def cancel ( canceler , other , message , canceled_by_admin = false )
2017-07-17 00:38:40 +00:00
2018-01-27 22:18:04 +00:00
cancel_tracking ( canceler , message , canceled_by_admin )
2017-07-17 00:38:40 +00:00
self . active = false
2016-04-06 02:23:15 +00:00
success = save
if success
2017-07-17 00:38:40 +00:00
lesson_sessions . upcoming . each do | lesson_session |
2016-04-21 14:23:29 +00:00
lesson_session = LessonSession . find ( lesson_session . id ) # because .upcoming creates ReadOnly records
2018-01-27 22:18:04 +00:00
lesson_session . cancel_lesson ( canceler , message , canceled_by_admin )
2016-04-21 14:23:29 +00:00
if ! lesson_session . save
return lesson_session
end
end
2016-04-06 02:23:15 +00:00
if approved_before?
2016-10-03 02:51:34 +00:00
# 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 "
2016-04-06 02:23:15 +00:00
else
2018-01-27 22:18:04 +00:00
if canceled_by_admin
# meh. no two way comm here. just bail
#Notification.send_lesson_message('canceled', next_lesson, true)
UserMailer . student_lesson_booking_declined ( self , message ) . deliver_now
UserMailer . teacher_lesson_booking_canceled ( self , message ) . deliver_now
elsif canceler == student
2016-04-06 02:23:15 +00:00
# if it's the first time acceptance student canceling, we call it a 'cancel'
Notification . send_lesson_message ( 'canceled' , next_lesson , false )
2016-07-17 15:16:27 +00:00
UserMailer . teacher_lesson_booking_canceled ( self , message ) . deliver_now
2016-04-21 14:23:29 +00:00
purpose = " Lesson Canceled "
2016-04-06 02:23:15 +00:00
else
# if it's the first time acceptance teacher, it was declined
2016-07-17 15:16:27 +00:00
UserMailer . student_lesson_booking_declined ( self , message ) . deliver_now
2016-04-06 02:23:15 +00:00
Notification . send_lesson_message ( 'declined' , next_lesson , true )
2016-04-21 14:23:29 +00:00
purpose = " Lesson Declined "
2016-04-06 02:23:15 +00:00
end
end
2016-04-21 14:23:29 +00:00
message = '' if message . nil?
2018-01-27 22:18:04 +00:00
if ! canceled_by_admin
msg = ChatMessage . create ( canceler , nil , message , ChatMessage :: CHANNEL_LESSON , nil , other , next_lesson , purpose )
end
2016-05-20 02:42:27 +00:00
else
2016-04-06 02:23:15 +00:00
end
2016-05-20 02:42:27 +00:00
2016-04-21 14:23:29 +00:00
self
2016-04-06 02:23:15 +00:00
end
def card_approved
self . card_presumed_ok = true
2017-07-10 02:21:29 +00:00
if posa_card_id
self . posa_card_purchased = true
end
2016-04-06 02:23:15 +00:00
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?
2016-10-03 02:51:34 +00:00
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
2018-02-25 22:28:12 +00:00
# we don't want to restrict this anymore. just let'em go to same teacher
# errors.add(:user, "have an in-progress or successful TestDrive with this teacher already")
2016-10-03 02:51:34 +00:00
end
2016-04-06 02:23:15 +00:00
end
2016-05-20 02:42:27 +00:00
2016-04-06 02:23:15 +00:00
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
2016-06-03 04:32:09 +00:00
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
2016-04-06 02:23:15 +00:00
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
2016-05-11 21:03:55 +00:00
def validate_uncollectables
if user . uncollectables . count > 0
errors . add ( :user , 'have unpaid lessons.' )
end
end
2016-04-06 02:23:15 +00:00
2016-05-12 21:29:27 +00:00
def school_owned?
! ! school
end
2016-06-03 04:32:09 +00:00
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
2016-10-03 02:51:34 +00:00
2016-04-06 02:23:15 +00:00
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
2016-06-03 04:32:09 +00:00
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 )
2016-04-06 02:23:15 +00:00
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
2016-06-03 04:32:09 +00:00
def self . book ( user , teacher , lesson_type , lesson_booking_slots , recurring , lesson_length , payment_style , description , test_drive_package_choice = nil )
2016-04-06 02:23:15 +00:00
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
2016-09-27 02:56:12 +00:00
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
2017-07-10 02:21:29 +00:00
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
2016-09-27 02:56:12 +00:00
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
2016-05-12 21:29:27 +00:00
if lesson_booking . teacher && lesson_booking . teacher . teacher . school
2016-05-12 22:41:25 +00:00
lesson_booking . school = lesson_booking . teacher . teacher . school
2016-05-12 21:29:27 +00:00
end
2016-04-06 02:23:15 +00:00
2017-01-06 12:43:38 +00:00
# copy payment settings from retailer into lesson booking
if lesson_booking . teacher && lesson_booking . teacher . teacher . retailer
2017-01-17 18:24:49 +00:00
lesson_booking . retailer = lesson_booking . teacher . teacher . retailer
2017-01-06 12:43:38 +00:00
lesson_booking . payment = lesson_booking . teacher . teacher . retailer . payment_details . to_json
2017-01-17 18:24:49 +00:00
lesson_booking . same_retailer = lesson_booking . teacher . teacher . retailer . affiliate_partner == user . affiliate_referral
2017-01-06 12:43:38 +00:00
end
2016-05-16 16:39:20 +00:00
if user
lesson_booking . same_school = ! ! ( lesson_booking . school && user . school && ( lesson_booking . school . id == user . school . id ) )
2016-09-08 11:06:28 +00:00
if lesson_booking . same_school
2017-03-22 12:39:06 +00:00
lesson_booking . same_school_free = false # !user.school.education # non-education schools (music schools) are 'free' when school-on-school
2016-09-08 11:06:28 +00:00
end
2018-02-15 04:16:32 +00:00
user . update_timezone ( lesson_booking_slots [ 0 ] . timezone ) if lesson_booking_slots . length > 0
2016-05-16 16:39:20 +00:00
else
lesson_booking . same_school = false
2016-09-08 10:59:58 +00:00
lesson_booking . same_school_free = false
2016-05-16 16:39:20 +00:00
end
2016-04-06 02:23:15 +00:00
# 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
2018-02-15 04:16:32 +00:00
description = '' if description . nil?
2016-04-21 14:23:29 +00:00
msg = ChatMessage . create ( user , lesson_booking . lesson_sessions [ 0 ] , description , ChatMessage :: CHANNEL_LESSON , nil , teacher , lesson_booking . lesson_sessions [ 0 ] , 'Lesson Requested' )
2016-04-06 02:23:15 +00:00
end
end
lesson_booking
end
def self . unprocessed ( current_user )
2017-07-10 02:21:29 +00:00
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)' )
2016-04-06 02:23:15 +00:00
end
def self . requested ( current_user )
LessonBooking . where ( user_id : current_user . id ) . where ( status : STATUS_REQUESTED )
end
2016-05-12 22:41:25 +00:00
def school_on_school?
2016-05-16 16:39:20 +00:00
same_school
2016-05-12 22:41:25 +00:00
end
2016-04-06 02:23:15 +00:00
2016-09-08 10:59:58 +00:00
def school_on_school_payment?
2017-03-22 12:39:06 +00:00
#!!(same_school && (school.education || school.is_guitar_center?))
same_school
2016-09-08 10:59:58 +00:00
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
2016-05-29 16:17:26 +00:00
def school_and_teacher
if school && school . scheduling_comm?
2016-05-29 19:37:54 +00:00
[ school . communication_email , teacher . email ]
2016-05-29 16:17:26 +00:00
else
[ teacher . email ]
end
end
2016-05-29 19:37:54 +00:00
def school_and_teacher_ids
if school && school . scheduling_comm?
[ school . owner . id , teacher . id ]
else
[ teacher . id ]
end
end
2016-05-29 16:17:26 +00:00
def school_over_teacher
if school && school . scheduling_comm?
2016-05-29 19:37:54 +00:00
[ school . communication_email ]
2016-05-29 16:17:26 +00:00
else
[ teacher . email ]
end
end
2016-05-29 19:37:54 +00:00
def school_over_teacher_ids
if school && school . scheduling_comm?
[ school . owner . id ]
else
[ teacher . id ]
end
end
2016-04-06 02:23:15 +00:00
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
2017-07-10 02:21:29 +00:00
# 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'
2016-04-06 02:23:15 +00:00
bill_monthlies
end
2017-07-10 02:21:29 +00:00
2016-04-06 02:23:15 +00:00
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 )
2016-09-08 10:59:58 +00:00
. where ( same_school_free : false )
2016-04-06 02:23:15 +00:00
. 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
2016-05-20 02:42:27 +00:00
def self . not_failed
end
2016-10-03 02:51:34 +00:00
2016-04-06 02:23:15 +00:00
def self . engaged_bookings ( student , teacher , since_at = nil )
bookings = bookings ( student , teacher , since_at )
2016-05-20 02:42:27 +00:00
bookings . engaged_or_successful
2016-04-06 02:23:15 +00:00
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
2016-05-29 16:17:26 +00:00
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
2016-04-06 02:23:15 +00:00
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
2016-05-29 16:17:26 +00:00
private
def school_scheduling_comm?
school ? school . school_comm? : false
end
2016-04-06 02:23:15 +00:00
end
end