From 0e1c070c898f1e50b005d8482227723fda3f35a7 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 3 Mar 2016 13:01:47 -0600 Subject: [PATCH] * wip --- db/up/lessons.sql | 9 +- pb/src/client_container.proto | 4 +- ruby/Gemfile | 1 + ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 49 ++++ .../student_lesson_counter.html.erb | 17 ++ .../student_lesson_counter.text.erb | 3 + .../teacher_lesson_counter.html.erb | 17 ++ .../teacher_lesson_counter.text.erb | 3 + ruby/lib/jam_ruby/message_factory.rb | 8 +- ruby/lib/jam_ruby/models/lesson_booking.rb | 52 ++-- .../jam_ruby/models/lesson_booking_slot.rb | 102 ++++++- .../models/lesson_package_purchase.rb | 2 +- ruby/lib/jam_ruby/models/lesson_session.rb | 63 ++++- .../models/lesson_session_analyser.rb | 248 ++++++++++++++++++ .../models/music_session_user_history.rb | 4 + ruby/lib/jam_ruby/models/notification.rb | 44 +--- ruby/lib/jam_ruby/models/user.rb | 2 + ruby/spec/factories.rb | 2 +- .../jam_ruby/flows/testdrive_lesson_spec.rb | 97 ++++++- .../models/lesson_session_analyser_spec.rb | 146 +++++++++++ ruby/spec/support/utilities.rb | 14 +- web/config/application.rb | 8 +- 23 files changed, 827 insertions(+), 69 deletions(-) create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb create mode 100644 ruby/lib/jam_ruby/models/lesson_session_analyser.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb diff --git a/db/up/lessons.sql b/db/up/lessons.sql index 974ee5272..26caee352 100644 --- a/db/up/lessons.sql +++ b/db/up/lessons.sql @@ -60,6 +60,8 @@ CREATE TABLE lesson_sessions ( ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200); +ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN; INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00); INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00); @@ -68,19 +70,22 @@ INSERT INTO lesson_package_types (id, name, description, package_type, price) VA CREATE TABLE lesson_booking_slots ( id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), - lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id) NOT NULL, + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), slot_type VARCHAR(64) NOT NULL, preferred_day DATE, day_of_week INTEGER, hour INTEGER, minute INTEGER, timezone VARCHAR NOT NULL, + update_all BOOLEAN NOT NULL DEFAULT FALSE, + proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); -ALTER TABLE lesson_sessions ADD COLUMN slot VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id); ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id); diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 9adacdae5..5652bfde7 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -652,13 +652,13 @@ message MixdownSignFailed { } message LessonMessage { - optional string lesson_session_id = 1; + optional string music_session_id = 1; optional string photo_url = 2; optional string msg = 3; optional string notification_id = 4; optional string created_at = 5; optional string sender_id = 6; - optional string received_id = 7; + optional string receiver_id = 7; optional bool student_directed = 8; optional string purpose = 9; optional string sender_name = 10; diff --git a/ruby/Gemfile b/ruby/Gemfile index a4135cd06..98fe9da76 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -55,6 +55,7 @@ gem 'sendgrid_toolkit', '>= 1.1.1' gem 'stripe' gem 'zip-codes' gem 'icalendar' +gem 'timespan' group :test do gem 'simplecov', '~> 0.7.1' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 1a0468f53..6d73ad030 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -278,6 +278,7 @@ require "jam_ruby/models/jamblaster" require "jam_ruby/models/jamblaster_user" require "jam_ruby/models/jamblaster_pairing_request" require "jam_ruby/models/sale_receipt_ios" +require "jam_ruby/models/lesson_session_analyser" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index fba85bdba..e0454212a 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -751,5 +751,54 @@ format.html end end + + # teacher proposed counter time; so send msg to the student + def student_lesson_counter(lesson_session, slot) + + email = lesson_session.student.email + subject = "Instructor has proposed a different time for your lesson" + unique_args = {:type => "student_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + # student proposed counter time; so send msg to the teacher + def teacher_lesson_counter(lesson_session, slot) + + email = lesson_session.teacher.email + subject = "Student has proposed a different time for their lesson" + unique_args = {:type => "teacher_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb new file mode 100644 index 000000000..38ec8a404 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@teacher.name} has proposed a different time for your lesson") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @teacher.name %> has proposed a different time for your lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb new file mode 100644 index 000000000..67972c906 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @teacher.name %> has proposed a different time for your lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb new file mode 100644 index 000000000..765f13b99 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "#{@student.name} has proposed a different time for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> has proposed a different time for their lesson request. +
+
+ Click the button below to get more information and respond. +

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb new file mode 100644 index 000000000..050917a31 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @student.name %> has proposed a different time for their lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index fbb591efa..613a7733d 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -917,7 +917,7 @@ module JamRuby end # creates the general purpose text message - def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, lesson_session_id, created_at) + def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose) lesson_message = Jampb::LessonMessage.new( :photo_url => sender_photo_url, :sender_name => sender_name, @@ -925,8 +925,10 @@ module JamRuby :receiver_id => receiver_id, :msg => msg, :notification_id => notification_id, - :lesson_session_id => lesson_session_id, - :created_at => created_at + :music_session_id => music_session_id, + :created_at => created_at, + :student_directed => student_directed, + :purpose => purpose ) Jampb::ClientMessage.new( diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 3ba3b02cd..595cc0478 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -4,7 +4,7 @@ module JamRuby @@log = Logging.logger[LessonBooking] - attr_accessor :accepting + attr_accessor :accepting, :countering, :countered_slot, :countered_lesson STATUS_REQUESTED = 'requested' STATUS_CANCELED = 'canceled' @@ -51,6 +51,7 @@ module JamRuby validate :validate_payment_style validate :validate_accepted, :if => :accepting + before_save :before_save before_validation :before_validation after_create :after_create @@ -70,23 +71,31 @@ module JamRuby def before_save automatically_default_slot - sync_remaining_test_drives end def after_save sync_lessons + sync_remaining_test_drives end def student user end - def accept(lesson_session, slot, update_all) + def accept(lesson_session, slot) + self.accepting = true + self.default_slot = slot - if self.default_slot.nil? || update_all - self.accepting = true - self.default_slot = slot - self.save! + if !self.save + raise ActiveRecord::Rollback + end + end + + def counter(lesson_session, proposer, slot) + self.countering = true + self.lesson_booking_slots << slot + if !self.save + raise ActiveRecord::Rollback end end @@ -100,7 +109,6 @@ module JamRuby def sync_remaining_test_drives if is_test_drive? || is_single_free? - puts "CARD PRESUMED OK card_presumed_ok: #{card_presumed_ok}, user_decremented: #{user_decremented}" if card_presumed_ok && !user_decremented self.user_decremented = true @@ -110,7 +118,6 @@ module JamRuby user.remaining_free_lessons = remaining_free_lessons elsif is_test_drive? - puts "TEST DRIVE DECREMENT" remaining_test_drives = user.remaining_test_drives - 1 User.where(id: user.id).update_all(remaining_test_drives: remaining_test_drives) user.remaining_test_drives = remaining_test_drives @@ -120,6 +127,10 @@ module JamRuby end end + def create_minimum_booking_time + (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + end + def sync_lessons if is_canceled? @@ -129,14 +140,20 @@ module JamRuby if !recurring && lesson_sessions.count == 1 - # if not recurring, and there is already one lession created, we are good + # if not recurring, and there is already one lesson created, then at most we may have to deal with a changed slot + if default_slot_changed? + if !lesson_sessions[0].update_scheduled_start(0) + puts "unable to update scheduled lesson" + raise ActiveRecord::Rollback + end + end return 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 = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + 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) @@ -225,11 +242,11 @@ module JamRuby def send_notices UserMailer.student_lesson_request(self).deliver UserMailer.teacher_lesson_request(self).deliver - LessonBooking.where(id: id).update_all(sent_notices: true) + self.sent_notices = true + self.save end def lesson_package_type - if is_single_free? LessonPackageType.single_free elsif is_test_drive? @@ -371,15 +388,20 @@ module JamRuby lesson_booking.sent_notices = false lesson_booking.teacher = teacher lesson_booking.lesson_type = lesson_type - lesson_booking.lesson_booking_slots = lesson_booking_slots 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 + end if lesson_booking_slots + if lesson_booking.save - msg = ChatMessage.create(user, nil, description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking) + msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking) end end lesson_booking diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb index 7e020f17d..91a1b7ea2 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -5,7 +5,8 @@ module JamRuby @@log = Logging.logger[LessonBookingSlot] belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" - + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :proposer, class_name: "JamRuby::User" has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot SLOT_TYPE_SINGLE = 'single' @@ -13,14 +14,49 @@ module JamRuby SLOT_TYPES = [SLOT_TYPE_SINGLE, SLOT_TYPE_RECURRING] + validates :proposer, presence: true validates :slot_type, inclusion: {in: SLOT_TYPES} #validates :preferred_day validates :day_of_week, numericality: {only_integer: true}, allow_blank: true # 0 = sunday - 6 = saturday validates :hour, numericality: {only_integer: true} validates :minute, numericality: {only_integer: true} validates :timezone, presence: true # example: 'America/New_York' + validates :update_all, inclusion: {in: [true, false]} validate :validate_slot_type + validate :validate_slot_minimum_time, on: :create + validate :validate_proposer + before_validation :before_validation + + def before_validation + if proposer.nil? + self.proposer = container.student + end + end + + def container + if lesson_booking + lesson_booking + else + lesson_session + end + end + + def is_teacher_created? + self.proposer == container.teacher + end + + def recipient + if is_teacher_created? + container.student + else + container.teacher + end + end + + def create_minimum_booking_time + (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60 * 60) + end def scheduled_times(needed_sessions, minimum_start_time) @@ -73,6 +109,52 @@ module JamRuby time end + def lesson_length + safe_lesson_booking.lesson_length + end + + def safe_lesson_booking + found = lesson_booking + found ||= lesson_session.lesson_booking + end + + def pretty_scheduled_start(with_timezone) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{tz}" + else + "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" + end + + + end + + def validate_proposer + if proposer && (proposer != container.student && proposer != container.teacher) + errors.add(:proposer, "must be either the student or teacher") + end + end + def validate_slot_type if slot_type == SLOT_TYPE_SINGLE if preferred_day.nil? @@ -86,5 +168,23 @@ module JamRuby end end end + + def validate_slot_minimum_time + if is_teacher_created? + return # the thinking is that a teacher can propose much tighter to the time; since they only counter; maybe they talked to the student + end + minimum_start_time = create_minimum_booking_time + + if day_of_week + # this is recurring; it will sort itself out + else + time = scheduled_time(0) + + if time <= minimum_start_time + errors.add("must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours out from now") + end + end + + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index 20999bbc3..b400df059 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -24,7 +24,7 @@ module JamRuby if self.lesson_package_type.is_test_drive? new_test_drives = user.remaining_test_drives + 4 User.where(id:user.id).update_all(remaining_test_drives: new_test_drives) - user.remaining_test_drives = user.remaining_test_drives + 4 + user.remaining_test_drives = new_test_drives end end diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index 14801ad1b..46355a17e 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -3,7 +3,7 @@ module JamRuby class LessonSession < ActiveRecord::Base - attr_accessor :accepting, :creating + attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson @@log = Logging.logger[LessonSession] @@ -24,6 +24,8 @@ module JamRuby belongs_to :teacher, class_name: "JamRuby::User" belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" validates :duration, presence: true, numericality: {only_integer: true} validates :lesson_booking, presence: true @@ -37,6 +39,26 @@ module JamRuby validate :validate_creating, :if => :creating validate :validate_accepted, :if => :accepting + after_save :after_counter, :if => :countering + after_save :manage_slot_changes, :if => :slot_changed? + + def manage_slot_changes + # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . + # TODO: what to do, what to do. + end + + def after_counter + send_counter(@countered_lesson, @countered_slot) + end + + def send_counter(countered_lesson, countered_slot) + if countered_slot.is_teacher_created? + UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver + else + UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver + end + self.countering = false + end default_scope { order(:created_at) } @@ -56,6 +78,7 @@ module JamRuby status == STATUS_APPROVED end + def validate_creating if !is_requested? && !is_approved? self.errors.add(:status, "is not valid for a new lesson session.") @@ -67,6 +90,7 @@ module JamRuby self.errors.add(:status, "This session is already #{self.status}.") end + self.accepting = false self.status = STATUS_APPROVED end @@ -124,30 +148,57 @@ module JamRuby end end + def update_scheduled_start(week_offset) + music_session.scheduled_start = default_slot.scheduled_time(week_offset) + music_session.save + end + # teacher accepts the lesson def accept(params) LessonSession.transaction do message = params[:message] slot = params[:slot] - update_all = params[:update_all] self.accepting = true slot = LessonBookingSlot.find(slot) - lesson_booking.accept(self, slot, update_all) + lesson_booking.accept(self, slot) self.slot = slot if self.save - msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, user, self) + msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking) Notification.send_lesson_message('accept', self, true) - UserMailer.student_lesson_accepted(self, message) - UserMailer.teacher_lesson_accepted(self, message) + UserMailer.student_lesson_accepted(self, message).deliver + UserMailer.teacher_lesson_accepted(self, message).deliver end end end + def counter(params) + proposer = params[:proposer] + slot = params[:slot] + message = params[:message] + + self.countering = true + slot.proposer = proposer + slot.lesson_session = self + self.lesson_booking_slots << slot + self.countered_slot = slot + self.countered_lesson = self + if self.save + if slot.update_all || lesson_booking.is_requested? + lesson_booking.counter(self, proposer, slot) + end + else + raise ActiveRecord::Rollback + end + + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + Notification.send_lesson_message('counter', self, slot.is_teacher_created?) + end + def home_url APP_CONFIG.external_root_url + "/client#/jamclass" end diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb new file mode 100644 index 000000000..0dfcf0818 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -0,0 +1,248 @@ +require 'timespan' +module JamRuby + class LessonSessionAnalyser + + SUCCESS = 'success' + SESSION_ONGOING = 'session_ongoing' + THRESHOLD_MET = 'threshold_met' + WAITED_CORRECTLY = 'waited_correctly' + MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently + LATE_CANCELLATION = 'late_cancellation' + + TEACHER_FAULT = 'teacher_fault' + STUDENT_FAULT = 'student_fault' + BOTH_FAULT = 'both_fault' + + + # what are the potential results? + + # bill: true/false + + # teacher: 'no_show' + # teacher: 'late' + # teacher: 'early_leave' + # teacher: 'waited_correctly' + # teacher: 'late_cancellation' + + # student: 'no_show' + # student: 'late' + # student: 'early_leave' + # student: 'minimum_time_not_met' + # student: 'threshold_met' + + + # reason: 'session_ongoing' + # reason: 'success' + # reason: 'student_fault' + # reason: 'teacher_fault' + # reason: 'both_fault' + + + def self.analyse(lesson_session) + reason = nil + teacher = nil + student = nil + bill = false + + music_session = lesson_session.music_session + + student_histories = MusicSessionUserHistory.where(music_session: music_session, user: lesson_session.student) + teacher_histories = MusicSessionUserHistory.where(music_session: music_session, user: lesson_session.teacher) + + # create ranges from music session user history + all_student_ranges = time_ranges(student_histories) + all_teacher_ranges = time_ranges(teacher_histories) + + # flatten ranges into non-overlapping ranges to simplifly logic + student_ranges = merge_overlapping_ranges(all_student_ranges) + teacher_ranges = merge_overlapping_ranges(all_teacher_ranges) + + intersecting = intersecting_ranges(student_ranges, teacher_ranges) + + student_analysis = analyse_intersection(lesson_session, student_ranges) + teacher_analysis = analyse_intersection(lesson_session, teacher_ranges) + together_analysis = analyse_intersection(lesson_session, intersecting) + + # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest + + if music_session.session_removed_at.nil? + reason = SESSION_ONGOING + teacher = SESSION_ONGOING + student = SESSION_ONGOING + bill = false + else + + if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? + # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. + reason = TEACHER_FAULT + teacher = LATE_CANCELLATION + student = nil + bill = false + elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? + # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. + reason = STUDENT_FAULT + teacher = nil + student = LATE_CANCELLATION + bill = true + elsif together_analysis[:pct] >= APP_CONFIG.lesson_together_threshold_pct + # If the teacher and the student are together in the lesson session for at least 80% of the scheduled lesson duration, regardless of who joined/left when, then we will bill the student. + bill = true + reason = SUCCESS + teacher = THRESHOLD_MET + student = THRESHOLD_MET + else + if teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] + # if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes; + # and if either: + + if student_analysis[:no_show] + # the student no-showed entirely, then we bill the student. + + elsif student_analysis[:joined_late] + # the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student + + elsif student_analysis[:joined_on_time] + # the student joined the session within 10 minutes after the teacher did, and the teacher was still there, and the teacher stays until the scheduled end time of the lesson; then we bill the student. + if teacher_analysis[:minimum_time_met] && teacher_analysis[:there_when_other_joined] + + else + + end + else + raise "unknown condition for lesson session: #{lesson_session.id}" + end + + elsif student_analysis[:joined_on_time] + if student_analysis[:waited_correctly] + if teacher_analysis[:other_there_when_joined] + # bill the student + else + + end + + end + + end + + # if the teacher meets the time threshold, then the user is getting billed + + teacher = MINIMUM_TIME_MET + bill = true + + # What happened with the student? + + if student_analysis[:waited_correctly] + student = WAITED_CORRECTLY + reason = STUDENT_FAULT + elsif student_analysis[:time] == 0 + student = NO_SHOW + reason = STUDENT_FAULT + elsif student_analysis + end + end + + end + + { + reason: reason, + teacher: teacher, + student: student, + bill: bill, + student_ranges: student_ranges, + teacher_ranges: teacher_ranges, + intersecting: intersecting, + student_analysis: student_analysis, + teacher_analysis: teacher_analysis, + together_analysis: together_analysis, + + } + end + + def self.intersecting_ranges(ranges_a, ranges_b) + intersections = [] + ranges_a.each do |range_a| + ranges_b.each do |range_b| + intersection = intersect(range_a, range_b) + intersections << intersection if intersection + end + end + + merge_overlapping_ranges(intersections) + end + + def self.analyse_intersection(lesson_session, ranges) + start = lesson_session.music_session.scheduled_start + + planned_duration_seconds = lesson_session.duration * 60 + measurable_start = start - (APP_CONFIG.lesson_analysis_slush_time_minutes * 60) + measurable_end = (start + planned_duration_seconds) + (APP_CONFIG.lesson_analysis_slush_time_minutes * 60) + + session_time = Range.new(measurable_start, measurable_end) + + # let's see how much time they spent together, irrespective of scheduled time + # and also, based on scheduled time + total = 0 + in_scheduled_time = 0 + + ranges.each do |range| + time = range.end - range.begin + + total += time + + in_session_time = intersect(range, session_time) + + if in_session_time + in_scheduled_time += in_scheduled_time.end - in_scheduled_time.begin + end + end + + + + # percentage computation of time spent during the session time + in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f + + { + total: total, + time: in_scheduled_time, + pct: in_scheduled_percentage + } + end + + def self.intersect(a, b) + min, max = a.first, a.exclude_end? ? a.max : a.last + other_min, other_max = b.first, b.exclude_end? ? b.max : b.last + + new_min = a === other_min ? other_min : b === min ? min : nil + new_max = a === other_max ? other_max : b === max ? max : nil + + new_min && new_max ? Range.new(new_min, new_max) : nil + end + + def self.time_ranges(histories) + ranges = [] + histories.each do |history| + ranges << history.range + end + ranges + end + + def self.ranges_overlap?(a, b) + a.include?(b.begin) || b.include?(a.begin) + end + + def self.merge_ranges(a, b) + [a.begin, b.begin].min..[a.end, b.end].max + end + + def self.merge_overlapping_ranges(ranges) + ranges.sort_by(&:begin).inject([]) do |ranges, range| + if !ranges.empty? && ranges_overlap?(ranges.last, range) + ranges[0...-1] + [merge_ranges(ranges.last, range)] + else + ranges + [range] + end + end + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index c8b8c0834..1fe327ba9 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -25,6 +25,10 @@ module JamRuby user.name end + def range + Range.new(created_at, session_removed_at || Time.now) + end + def music_session @msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id) end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index b78eceea6..dea478c7d 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -377,17 +377,17 @@ module JamRuby notification = Notification.new notification.description = NotificationTypes::LESSON_MESSAGE notification.student_directed = student_directed + if !student_directed - notification.source_user_id = lesson_session.user.id + notification.source_user_id = lesson_session.student.id notification.target_user_id = lesson_session.teacher.id else notification.source_user_id = lesson_session.teacher.id - notification.target_user_id = lesson_session.user.id + notification.target_user_id = lesson_session.student.id end notification.purpose = purpose - notification.lesson_session = lesson_session - notification.session_id = lesson_session.music_session_id + notification.session_id = lesson_session.music_session.id notification_msg = 'Lesson Changed' @@ -412,37 +412,21 @@ module JamRuby notification.save # receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at - message = @message_factory.lesson_message( - notification.target.id, - notification.source.resolved_photo_url, - notification.source.name, - notification.source.id, + message = @@message_factory.lesson_message( + notification.target_user.id, + notification.source_user.resolved_photo_url, + notification.source_user.name, + notification.source_user.id, notification_msg, notification.id, - notification.lesson_session.id, - notification.created_date + notification.session_id, + notification.created_date, + notification.student_directed, + notification.purpose ) - if follower.id != user.id - if user.online - msg = @@message_factory.new_user_follower( - user.id, - follower.photo_url, - notification_msg, - notification.id, - notification.created_date - ) + @@mq_router.publish_to_user(notification.target_user.id, message) - @@mq_router.publish_to_user(user.id, msg) - - else - begin - UserMailer.new_user_follower(user, notification_msg).deliver - rescue => e - @@log.error("Unable to send NEW_USER_FOLLOWER email to offline user #{user.email} #{e}") - end - end - end end def send_new_band_follower(follower, band) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 27379b747..79b35198f 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1898,8 +1898,10 @@ module JamRuby lesson = nil test_drive = nil User.transaction do + lesson = card_approved(params[:token], params[:zip]) if params[:test_drive] + self.reload test_drive = Sale.purchase_test_drive(self) end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index dc399cf3d..de32859ce 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -921,7 +921,7 @@ FactoryGirl.define do factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do factory :lesson_booking_slot_single do slot_type 'single' - preferred_day Date.today + preferred_day Date.today + 3 day_of_week nil hour 12 minute 30 diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb index 185210814..c419ec38f 100644 --- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -15,23 +15,26 @@ describe "TestDrive Lesson Flow" do it "works" do - # user has no test drives, but attempts to book a lesson + # user has no test drives, no credit card on file, but attempts to book a lesson booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") - puts booking.errors.inspect booking.errors.any?.should be_false booking.card_presumed_ok.should be_false booking.user.should eql user booking.card_presumed_ok.should be_false booking.should eql user.unprocessed_test_drive + booking.sent_notices.should be_false - # so, they still need validate their take + ########## Need validate their credit card token = create_stripe_token - puts "UPDATING PAYMENT" result = user.payment_update({token: token, zip: '78759', test_drive: true}) lesson = result[:lesson] test_drive = result[:test_drive] lesson.errors.any?.should be_false test_drive.errors.any?.should be_false + lesson.card_presumed_ok.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.sent_notices.should be_true test_drive.stripe_charge_id.should_not be_nil test_drive.recurly_tax_in_cents.should be 412 @@ -42,7 +45,6 @@ describe "TestDrive Lesson Flow" do line_item.quantity.should eql 1 line_item.product_type.should eql SaleLineItem::LESSON line_item.product_id.should eq LessonPackageType.test_drive.id - user.reload user.stripe_customer_id.should_not be nil user.lesson_purchases.length.should eql 1 @@ -53,5 +55,90 @@ describe "TestDrive Lesson Flow" do customer = Stripe::Customer.retrieve(user.stripe_customer_id) customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Student has proposed a different time for your lesson." + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 2 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Your lesson request is confirmed!" + + + # teacher & student get into session + + LessonSession end end diff --git a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb new file mode 100644 index 000000000..b57939817 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe LessonSessionAnalyser do + + let(:user) {FactoryGirl.create(:user)} + let(:music_session) { FactoryGirl.create(:music_session, creator: user) } + let(:start) {Time.now} + + describe "intersecting_ranges" do + + it "empty" do + LessonSessionAnalyser.intersecting_ranges([], []).should eql [] + end + + it "one specified, other empty" do + + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([], [Range.new(uh1Begin, uh1End)]).should eql [] + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], []).should eql [] + end + + it "both identical" do + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh1End)] + end + + it "one intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 1 + uh2End = start + 3 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [Range.new(uh2Begin, uh2End)] + end + + it "overlapping intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + -1 + uh2End = start + 2 + + uh3Begin = start + 3 + uh3End = start + 5 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh2Begin, uh2End), Range.new(uh3Begin, uh3End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh2End), Range.new(uh3Begin, uh1End)] + end + + it "no overlap" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 5 + uh2End = start + 6 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [] + end + end + + describe "merge_overlapping_ranges" do + + it "empty" do + LessonSessionAnalyser.merge_overlapping_ranges([]).should eql [] + end + + it "one item" do + uh1Begin = start + uh1End = start + 1 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + + ranges = LessonSessionAnalyser.time_ranges [uh1] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two identical items" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = uh1Begin + uh2End = uh1End + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two separate times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 3 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End), Range.new(uh2Begin, uh2End)] + end + + it "two overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh2End)] + end + + it "three overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh3Begin = start + 0.1 + uh3End = start + 6 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + uh3 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh3Begin, session_removed_at: uh3End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2, uh3] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh3End)] + end + end +end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 44267d760..f1bb62842 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -259,7 +259,19 @@ def app_config end def minimum_lesson_booking_hrs - 48 + 24 + end + + def lesson_analysis_slush_time_minutes + 5 + end + + def lesson_stay_time + 10 + end + + def lesson_together_threshold_pct + 0.8 end private diff --git a/web/config/application.rb b/web/config/application.rb index 7db2d108d..934026d17 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -426,6 +426,10 @@ if defined?(Bundler) :publishable_key => 'pk_test_9vO8ZnxBpb9Udb0paruV3qLv', :secret_key => 'sk_test_cPVRbtr9xbMiqffV8jwibwLA' } - config.minimum_lesson_booking_hrs = 48 - end + config.minimum_lesson_booking_hrs = 24 + config.lesson_analysis_slush_time_minutes = 5 + config.lesson_stay_time = 10 + config.lesson_together_threshold_pct = 0.80 + + end end