827 lines
28 KiB
Ruby
827 lines
28 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, :countered_slot, :countered_lesson
|
|
|
|
STATUS_REQUESTED = 'requested'
|
|
STATUS_CANCELED = 'canceled'
|
|
STATUS_APPROVED = 'approved'
|
|
STATUS_SUSPENDED = 'suspended'
|
|
STATUS_COUNTERED = 'countered'
|
|
|
|
STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED]
|
|
|
|
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'
|
|
|
|
|
|
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 :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking
|
|
belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking
|
|
|
|
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
|
|
has_many :lesson_sessions, class_name: "JamRuby::LessonSession"
|
|
has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase"
|
|
|
|
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 :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_accepted, :if => :accepting
|
|
|
|
|
|
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("status = '#{STATUS_APPROVED}' OR status = '#{STATUS_REQUESTED}' OR status = '#{STATUS_SUSPENDED}'") }
|
|
|
|
def before_validation
|
|
if self.booked_price.nil?
|
|
self.booked_price = compute_price
|
|
end
|
|
end
|
|
|
|
def after_create
|
|
if card_presumed_ok && !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
|
|
{price: booked_price, real_price: booked_price, total_price: booked_price}
|
|
end
|
|
# here for shopping_cart
|
|
def price
|
|
booked_price
|
|
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
|
|
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
|
|
self.status = STATUS_APPROVED
|
|
self.counter_slot = nil
|
|
self.default_slot = slot
|
|
self.accepter = accepter
|
|
|
|
success = self.save
|
|
|
|
if !success
|
|
puts "unable to accept lesson booking #{errors.inspect}"
|
|
end
|
|
success
|
|
end
|
|
|
|
def counter(lesson_session, proposer, slot)
|
|
self.countering = true
|
|
self.lesson_booking_slots << slot
|
|
self.counter_slot = slot
|
|
#self.status = STATUS_COUNTERED
|
|
self.save
|
|
end
|
|
|
|
def automatically_default_slot
|
|
if is_requested?
|
|
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 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?
|
|
user.remaining_test_drives = user.remaining_test_drives - 1
|
|
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?
|
|
# 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").where("scheduled_start > ?", minimum_start_time).order(:created_at)
|
|
|
|
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
|
|
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 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_approved?
|
|
status == STATUS_APPROVED
|
|
end
|
|
|
|
def is_suspended?
|
|
status == STATUS_SUSPENDED
|
|
end
|
|
|
|
def is_active?
|
|
active
|
|
end
|
|
|
|
def validate_accepted
|
|
# accept is multipe purpose; either accept the initial request, or a counter slot
|
|
if self.status_was != STATUS_REQUESTED && counter_slot.nil? # && self.status_was != STATUS_COUNTERED
|
|
self.errors.add(:status, "This lesson is already #{self.status}.")
|
|
end
|
|
|
|
self.accepting = false
|
|
end
|
|
|
|
def send_notices
|
|
UserMailer.student_lesson_request(self).deliver
|
|
UserMailer.teacher_lesson_request(self).deliver
|
|
Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept'
|
|
self.sent_notices = true
|
|
self.save
|
|
end
|
|
|
|
def lesson_package_type
|
|
if is_single_free?
|
|
LessonPackageType.single_free
|
|
elsif is_test_drive?
|
|
LessonPackageType.test_drive
|
|
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?
|
|
LessonPackageType.test_drive.price
|
|
elsif is_normal?
|
|
teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY)
|
|
end
|
|
end
|
|
|
|
def distribution_price_in_cents
|
|
if is_single_free?
|
|
0
|
|
elsif is_test_drive?
|
|
10 * 100
|
|
elsif is_normal?
|
|
booked_price * 100
|
|
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}#{am_pm}"
|
|
|
|
end
|
|
|
|
def approved_before?
|
|
!self.accepter_id.nil?
|
|
end
|
|
def cancel(canceler, other, message)
|
|
|
|
self.active = false
|
|
self.status = STATUS_CANCELED
|
|
self.cancel_message = message
|
|
self.canceler = canceler
|
|
success = save
|
|
if success
|
|
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
|
|
UserMailer.teacher_lesson_booking_canceled(self, message).deliver
|
|
chat_message_prefix = "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
|
|
chat_message_prefix = "Lesson Canceled"
|
|
else
|
|
# if it's the first time acceptance teacher, it was declined
|
|
UserMailer.student_lesson_booking_declined(self, message).deliver
|
|
Notification.send_lesson_message('declined', next_lesson, true)
|
|
chat_message_prefix = "Lesson Declined"
|
|
end
|
|
end
|
|
|
|
chat_message = message.nil? ? chat_message_prefix : "#{chat_message_prefix}: #{message}"
|
|
msg = ChatMessage.create(canceler, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, other, self)
|
|
|
|
end
|
|
|
|
success
|
|
end
|
|
|
|
def card_approved
|
|
self.card_presumed_ok = true
|
|
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 user.has_requested_test_drive?(teacher) && !user.admin
|
|
errors.add(:user, "has a requested TestDrive with this teacher")
|
|
end
|
|
if !user.has_test_drives? && !user.can_buy_test_drive?
|
|
errors.add(:user, "have no remaining test drives")
|
|
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 lesson_booking_slots.length == 0 || lesson_booking_slots.length == 1
|
|
errors.add(:lesson_booking_slots, "must have two times specified")
|
|
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 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)
|
|
self.book(user, teacher, LessonBooking::LESSON_TYPE_TEST_DRIVE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description)
|
|
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)
|
|
|
|
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
|
|
|
|
# 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
|
|
msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking)
|
|
end
|
|
end
|
|
lesson_booking
|
|
end
|
|
|
|
def self.unprocessed(current_user)
|
|
LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false)
|
|
end
|
|
|
|
def self.requested(current_user)
|
|
LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED)
|
|
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
|
|
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)
|
|
.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.engaged_bookings(student, teacher, since_at = nil)
|
|
bookings = bookings(student, teacher, since_at)
|
|
bookings.engaged
|
|
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 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
|
|
end
|
|
end
|