jamclass enabled

This commit is contained in:
Seth Call 2016-05-16 11:39:20 -05:00
parent 5d82f555c5
commit 92b2e13ee8
50 changed files with 1003 additions and 149 deletions

File diff suppressed because one or more lines are too long

View File

@ -347,4 +347,5 @@ track_school_signups.sql
add_test_drive_types.sql
updated_subjects.sql
update_payment_history.sql
lesson_booking_schools.sql
lesson_booking_schools.sql
lesson_booking_schools_2.sql

View File

@ -0,0 +1,2 @@
ALTER TABLE lesson_bookings ADD COLUMN same_school BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE schools ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id);

View File

@ -52,7 +52,7 @@ class ArtifactUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(exe msi dmg)
%w(exe msi dmg zip)
end
# Override the filename of the uploaded files:

View File

@ -2,6 +2,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
self.table_name = 'affiliate_partners'
belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner
has_one :school, class_name: "JamRuby::School"
has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id
belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id
has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id
@ -62,7 +63,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
# used by admin
def self.create_with_params(params={})
raise 'not supported'
oo = self.new
oo = AffiliatePartner.new
oo.partner_name = params[:partner_name].try(:strip)
oo.partner_code = params[:partner_code].try(:strip).try(:downcase)
oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first
@ -74,15 +75,25 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
# used by web
def self.create_with_web_params(user, params={})
oo = self.new
oo = AffiliatePartner.new
oo.partner_name = params[:partner_name].try(:strip)
oo.partner_user = user if user # user is not required
oo.entity_type = params[:entity_type] || ENTITY_TYPES.first
oo.signed_at = Time.now
signed_legalese
oo.save
oo
end
def self.create_from_school(school)
oo = AffiliatePartner.new
oo.partner_name = "Affiliate from School #{school.id}"
oo.partner_user = school.owner
oo.entity_type = 'Other'
oo.school = school
oo.signed_at = nil
oo.save
end
def self.coded_id(code=nil)
self.where(:partner_code => code).limit(1).pluck(:id).first if code.present?
end
@ -126,7 +137,11 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
applicable_rate = shopping_cart.is_lesson? ? lesson_rate : rate
if shopping_cart.is_lesson?
applicable_rate = lesson_rate
else
applicable_rate = rate
end
{fee_in_cents: (product_info[:price] * 100 * real_quantity * applicable_rate.to_f).round}
else

View File

@ -9,7 +9,7 @@ module JamRuby
@@log = Logging.logger[LessonBooking]
attr_accessor :accepting, :countering, :canceling, :countered_slot, :countered_lesson
attr_accessor :accepting, :countering, :canceling, :countered_slot, :countered_lesson, :current_purchase, :current_lesson
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
@ -51,6 +51,7 @@ module JamRuby
validates :recurring, inclusion: {in: [true, false]}
validates :sent_notices, inclusion: {in: [true, false]}
validates :card_presumed_ok, inclusion: {in: [true, false]}
validates :same_school, inclusion: {in: [true, false]}
validates :active, inclusion: {in: [true, false]}
validates :lesson_length, inclusion: {in: [30, 45, 60, 90, 120]}
validates :payment_style, inclusion: {in: PAYMENT_STYLES}
@ -112,7 +113,20 @@ module JamRuby
# here for shopping_cart
def product_info
{price: booked_price, real_price: booked_price, total_price: booked_price}
if is_test_drive?
real_price = 0
elsif is_monthly_payment?
raise "no purchase assigned to lesson booking for monthly payment!" if current_purchase.nil?
real_price = self.current_purchase.teacher_distribution.jamkazam_margin
else
if current_lesson.nil?
puts "OHOHOMOOMG #{self.inspect}"
raise "no purchase assigned to lesson booking for lesson!"
end
real_price = self.current_lesson.teacher_distribution.jamkazam_margin
end
{price: real_price, real_price: real_price, total_price: real_price}
end
# here for shopping_cart
def price
@ -357,7 +371,9 @@ module JamRuby
end
def requires_teacher_distribution?(target)
if target.is_a?(JamRuby::LessonSession)
if school_on_school?
false
elsif target.is_a?(JamRuby::LessonSession)
is_test_drive? || (is_normal? && !is_monthly_payment?)
elsif target.is_a?(JamRuby::LessonPackagePurchase)
is_monthly_payment?
@ -694,6 +710,12 @@ module JamRuby
lesson_booking.school = lesson_booking.teacher.teacher.school
end
if user
lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id))
else
lesson_booking.same_school = false
end
# two-way association slots, for before_validation loic in slot to work
lesson_booking.lesson_booking_slots = lesson_booking_slots
lesson_booking_slots.each do |slot|
@ -718,7 +740,7 @@ module JamRuby
end
def school_on_school?
teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id)
same_school
end
def self.find_bookings_needing_sessions(minimum_start_time)
@ -760,6 +782,7 @@ module JamRuby
.joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND (lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}))")
.where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)")
.where(payment_style: PAYMENT_STYLE_MONTHLY)
.where(same_school: false)
.active
.where('music_sessions.scheduled_start >= ?', current_month_first_day)
.where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq

View File

@ -121,15 +121,20 @@ module JamRuby
user
end
# test drive purchase doesn't have a tea
def school_on_school?
teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id)
if teacher
teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id)
else
false
end
end
def bill_monthly(force = false)
if school_on_school?
success = true
raise "school-on-school: should not be here"
else
lesson_payment_charge.charge(force)
success = lesson_payment_charge.billed

View File

@ -11,7 +11,7 @@ module JamRuby
@@log = Logging.logger[LessonSession]
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, to: :lesson_booking
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, to: :lesson_booking
delegate :pretty_scheduled_start, to: :music_session
@ -176,13 +176,19 @@ module JamRuby
def billed
if lesson_booking.is_test_drive?
false
else
elsif lesson_payment_charge
lesson_payment_charge.billed
else
false
end
end
def amount_charged
lesson_payment_charge.amount_in_cents / 100.0
if lesson_payment_charge
lesson_payment_charge.amount_in_cents / 100.0
else
0.0
end
end
def self.analysis_to_json(analysis, preserve_object = false)
@ -398,10 +404,6 @@ module JamRuby
@parsed_analysis || analysis ? JSON.parse(analysis) : nil
end
def school_on_school?
teacher.teacher.school && student.school && (teacher.teacher.school.id == student.school.id)
end
def validate_creating
if !is_requested? && !is_approved?
self.errors.add(:status, "is not valid for a new lesson session.")
@ -490,7 +492,13 @@ module JamRuby
query = query.where('(lesson_sessions.teacher_id = ? or music_sessions.user_id = ?)', user.id, user.id)
end
query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?)', user.id)
if user.school_id
school_extra = "OR (lesson_bookings.school_id = '#{user.school_id}')"
else
school_extra = ''
end
query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?) ' + school_extra, user.id)
current_page = params[:page].nil? ? 1 : params[:page].to_i
next_page = current_page + 1

View File

@ -228,6 +228,11 @@ module JamRuby
if sale.valid?
if lesson_booking
lesson_booking.current_lesson = lesson_session
lesson_booking.current_purchase = lesson_package_purchase
end
sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)
price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force)
@ -251,10 +256,12 @@ module JamRuby
purchase = price_info[:purchase]
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
puts "invalid sale object"
raise "invalid sale object"
end
end
{sale: sale, stripe_charge: stripe_charge, purchase: purchase}
end

View File

@ -91,6 +91,15 @@ module JamRuby
line_item
end
def add_referral_if_needed(user, shopping_cart, lesson_booking)
# if the teacher came from an affiliate, this is our chance to account for that (the student's affiliate status was accounted for in create_from_shopping_cart)
referral_info = user.should_attribute_sale?(shopping_cart, lesson_booking)
if referral_info
self.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], self)
self.save!
end
end
# in a shopping-cart less world (ios purchase), let's reuse as much logic as possible
def self.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)
teacher = lesson_booking.teacher if lesson_booking
@ -98,12 +107,20 @@ module JamRuby
line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil, lesson_booking)
if lesson_booking
# if the teacher came from an affiliate, this is our chance to account for that (the student's affiliate status was accounted for in create_from_shopping_cart)
referral_info = teacher.should_attribute_sale?(shopping_cart, lesson_booking)
if referral_info
line_item.affiliate_distributions << AffiliateDistribution.create(teacher.affiliate_referral, referral_info[:fee_in_cents], line_item)
line_item.save!
teacher = lesson_booking.teacher
student = lesson_booking.student
if lesson_booking.is_test_drive?
# no referral for test drives
elsif lesson_booking.school_on_school?
# no referral; we don't make money on school-on-school
else
line_item.add_referral_if_needed(student, shopping_cart, lesson_booking)
if lesson_booking.school.nil?
line_item.add_referral_if_needed(teacher, shopping_cart, lesson_booking)
end
end
end
@ -131,7 +148,7 @@ module JamRuby
# determine if we need to associate this sale with a partner
user = shopping_cart.user
referral_info = user.should_attribute_sale?(shopping_cart, instance)
referral_info = user.should_attribute_sale?(shopping_cart, instance) if !instance || !instance.is_a?(LessonBooking) # all affiliate stuff is handled elsewhere
if referral_info
sale_line_item.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], sale_line_item)

View File

@ -15,6 +15,7 @@ module JamRuby
attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection
belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_school
belongs_to :affiliate_partner, class_name: "JamRuby::AffiliatePartner"
has_many :students, class_name: ::JamRuby::User
has_many :teachers, class_name: ::JamRuby::Teacher
has_many :school_invitations, class_name: 'JamRuby::SchoolInvitation'
@ -27,8 +28,12 @@ module JamRuby
validates :correspondence_email, email: true, allow_blank: true
validate :validate_avatar_info
after_create :create_affiliate
before_save :stringify_avatar_info, :if => :updating_avatar
def create_affiliate
AffiliatePartner.create_from_school(self)
end
def update_from_params(params)
self.name = params[:name] if params[:name].present?
self.scheduling_communication = params[:scheduling_communication] if params[:scheduling_communication].present?

View File

@ -58,6 +58,10 @@ module JamRuby
# only show teachers with ready for session set to true
query = query.where('teachers.ready_for_session_at IS NOT NULL')
if params[:onlyMySchool] && params[:onlyMySchool] != 'false' && user.school_id
query = query.where("teachers.school_id = ?", user.school_id)
end
instruments = params[:instruments]
if instruments && !instruments.blank? && instruments.length > 0
query = query.joins("inner JOIN teachers_instruments AS tinst ON tinst.teacher_id = teachers.id")

View File

@ -80,11 +80,37 @@ module JamRuby
'$%.2f' % real_distribution
end
def jamkazam_margin_in_cents
if is_test_drive?
0
else
if school
# if school exists, use it's rate
rate = school.jamkazam_rate
else
# otherwise use the teacher's rate
rate = teacher.teacher.jamkazam_rate
end
amount_in_cents * (rate)
end
end
def jamkazam_margin
(jamkazam_margin_in_cents / 100).round(2)
end
def calculate_teacher_fee
if is_test_drive?
0
else
(amount_in_cents * (teacher.teacher.jamkazam_rate + 0.03)).round
if school
# if school exists, use it's rate
rate = school.jamkazam_rate
else
# otherwise use the teacher's rate
rate = teacher.teacher.jamkazam_rate
end
(amount_in_cents * (rate + 0.03)).round
end
end
@ -116,7 +142,7 @@ module JamRuby
if lesson_session
lesson_session.timed_description
else
lesson_package_purchase.description
lesson_package_purchase.timed_description
end
end
end

View File

@ -70,7 +70,7 @@ module JamRuby
# bands
has_many :band_musicians, :class_name => "JamRuby::BandMusician"
has_many :bands, :through => :band_musicians, :class_name => "JamRuby::Band"
has_one :teacher, :class_name => "JamRuby::Teacher"
belongs_to :teacher, :class_name => "JamRuby::Teacher", foreign_key: :teacher_id
# genres
has_many :genre_players, as: :player, class_name: "JamRuby::GenrePlayer", dependent: :destroy
@ -1106,6 +1106,7 @@ module JamRuby
school_id = options[:school_id]
school_interest = options[:school_interest]
school = School.find(school_id) if school_id
user = User.new
user.validate_instruments = true
UserManager.active_record_transaction do |user_manager|
@ -1139,10 +1140,12 @@ module JamRuby
if school_id.present?
if user.is_a_student
user.school_id = school_id
user.affiliate_referral = school.affiliate_partner
elsif user.is_a_teacher
school = School.find_by_id(school_id)
school_name = school ? school.name : 'a music school'
user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Teaches for #{school_name}", school_id: school_id)
user.affiliate_referral = school.affiliate_partner
end
else
if user.is_a_teacher
@ -1869,6 +1872,7 @@ module JamRuby
return false
end
if affiliate_referral
referral_info = affiliate_referral.should_attribute_sale?(shopping_cart, self, instance)
else
@ -2109,10 +2113,22 @@ module JamRuby
end
def has_rated_teacher(teacher)
teacher_rating(teacher).count > 0
end
def has_rated_student(student)
student_rating(student).count > 0
end
def teacher_rating(teacher)
if teacher.is_a?(JamRuby::User)
teacher = teacher.teacher
end
Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s).count > 0
Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s)
end
def student_rating(student)
Review.where(target_id: student.id).where(target_type: "JamRuby::User")
end
def has_rated_student(student)

View File

@ -12,6 +12,10 @@ describe "Monthly Recurring Lesson Flow" do
let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) }
let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] }
let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] }
let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) }
let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) }
let(:school) {FactoryGirl.create(:school)}
after {Timecop.return}
@ -37,12 +41,15 @@ describe "Monthly Recurring Lesson Flow" do
########## Need validate their credit card
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', normal: true})
result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id})
puts "result #{result.inspect}"
booking.reload
booking.card_presumed_ok.should be_true
booking.errors.any?.should be_false
lesson.errors.any?.should be_false
booking = result[:lesson]
lesson = booking.lesson_sessions[0]
lesson.errors.any?.should be_false
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should be 0.0
@ -147,6 +154,9 @@ describe "Monthly Recurring Lesson Flow" do
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
Timecop.travel(end_time + 1)
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
@ -223,9 +233,259 @@ describe "Monthly Recurring Lesson Flow" do
lesson.amount_charged.should eql 0.0
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be false
lesson_session.sent_billing_notices.should be nil
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.length.should eql 1 # one for student
UserMailer.deliveries.length.should eql 0 # one for student
end
it "works (school on school)" do
# get user and teacher into same school
user.school = school
user.save!
teacher.school = school
teacher.save!
# if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on
Timecop.travel(Date.new(2016, 3, 20))
# user has no test drives, no credit card on file, but attempts to book a lesson
booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60)
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.sent_notices.should be_true
booking.booked_price.should eql 30.00
user.reload
user.stripe_customer_id.should be nil
user.remaining_test_drives.should eql 0
user.lesson_purchases.length.should eql 0
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, update_all: true)
UserMailer.deliveries.clear
lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'})
booking.reload
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
######### Student counters with new slot
student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16, update_all: true)
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
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false})
UserMailer.deliveries.each do |del|
# puts del.inspect
end
# get acceptance emails, as well as 'your stuff is accepted'
UserMailer.deliveries.length.should eql 2
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.purpose.should eql 'Lesson Approved'
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
# teacher & student get into session
start = lesson_session.scheduled_start
end_time = lesson_session.scheduled_start + (60 * lesson_session.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
Timecop.travel(end_time + 1)
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
if lesson_session.billing_error_detail
puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
# let user pay for it
LessonBooking.hourly_check
LessonPaymentCharge.count.should eql 0
TeacherDistribution.count.should eql 0
user.reload
user.lesson_purchases.length.should eql 0
user.sales.length.should eql 0
TeacherPayment.count.should eql 0
TeacherPayment.hourly_check
TeacherPayment.count.should eql 0
# teacher & student get into session
start = lesson_session.scheduled_start
end_time = lesson_session.scheduled_start + (60 * lesson_session.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
UserMailer.deliveries.clear
# background code comes around and analyses the session
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
if lesson_session.billing_error_detail
puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson_session.amount_charged.should eql 0.0
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be_nil
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.length.should eql 0 # one for student
LessonPaymentCharge.count.should eql 0
TeacherDistribution.count.should eql 0
end
it "affiliate gets their cut" do
user.affiliate_referral = affiliate_partner
user.save!
teacher_user.affiliate_referral = affiliate_partner2
teacher_user.save!
lesson = monthly_lesson(user, teacher_user, true)
user.reload
user.lesson_purchases.count.should eql 1
lesson_package_purchase = user.lesson_purchases.first
teacher_distribution = lesson_package_purchase.teacher_distribution
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2
affiliate_partner.affiliate_distributions.count.should eql 1
affiliate_partner2.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.first
partner2_distribution = affiliate_partner2.affiliate_distributions.first
partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item
partner1_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner.lesson_rate).round
partner2_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner2.lesson_rate).round
end
it "school affiliate gets nothing when teacher school is involved" do
teacher.school = school
teacher.save!
user.affiliate_referral = affiliate_partner
user.save!
lesson = monthly_lesson(user, teacher_user, true)
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 1
user.lesson_purchases.count.should eql 1
lesson_package_purchase = user.lesson_purchases.first
teacher_distribution = lesson_package_purchase.teacher_distribution
affiliate_partner.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.first
partner1_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * affiliate_partner.lesson_rate).round
school.affiliate_partner.affiliate_distributions.count.should eql 0
end
it "student school affiliates gets cut when student school is involved. so does teacher's" do
user.affiliate_referral = school.affiliate_partner
user.save!
teacher_user.affiliate_referral = affiliate_partner
teacher_user.save!
lesson = monthly_lesson(user, teacher_user, true)
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2
user.lesson_purchases.count.should eql 1
lesson_package_purchase = user.lesson_purchases.first
teacher_distribution = lesson_package_purchase.teacher_distribution
affiliate_partner.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.count.should eql 1
school.affiliate_partner.affiliate_distributions.count.should eql 1
school_partner_distribution = school.affiliate_partner.affiliate_distributions.first
school_partner_distribution.affiliate_referral_fee_in_cents.should eql (teacher_distribution.jamkazam_margin_in_cents * school.affiliate_partner.lesson_rate).round
end
end

View File

@ -12,8 +12,12 @@ describe "Normal Lesson Flow" do
let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) }
let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] }
let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] }
let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) }
let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) }
let(:school) {FactoryGirl.create(:school)}
after {Timecop.return}
describe "stripe mocked" do
before {
StripeMock.clear_errors
@ -22,7 +26,6 @@ describe "Normal Lesson Flow" do
teacher.save!
}
after { StripeMock.stop }
after {Timecop.return}
it "bill failure" do
@ -434,22 +437,18 @@ describe "Normal Lesson Flow" do
booking.school.should be_true
booking.card_presumed_ok.should be_false
booking.user.should eql user
user.unprocessed_normal_lesson.should eql []
booking.sent_notices.should be_false
user.unprocessed_normal_lesson.should be_nil
booking.sent_notices.should be_true
booking.booked_price.should eql 30.00
booking.is_requested?.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
LessonPaymentCharge.count.should eql 0
user.reload
user.stripe_customer_id.should_not be nil
user.stripe_customer_id.should be_nil
user.remaining_test_drives.should eql 0
user.lesson_purchases.length.should eql 0
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
@ -550,35 +549,75 @@ describe "Normal Lesson Flow" do
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson_session.billed.should be true
lesson_session.billed.should be false
user.reload
user.lesson_purchases.length.should eql 1
lesson_purchase = user.lesson_purchases[0]
lesson_purchase.price.should eql 30.00
lesson_purchase.lesson_package_type.is_normal?.should eql true
lesson_purchase.price_in_cents.should eql 3000
user.sales.length.should eql 1
sale = user.sales.first
sale.stripe_charge_id.should_not be_nil
sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i
sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i
sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i
sale.recurly_currency.should eql 'USD'
sale.stripe_charge_id.should_not be_nil
line_item = sale.sale_line_items[0]
line_item.quantity.should eql 1
line_item.product_type.should eql SaleLineItem::LESSON
line_item.product_id.should eq LessonPackageType.single.id
line_item.lesson_package_purchase.should eql lesson_purchase
lesson_purchase.sale_line_item.should eql line_item
lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f
user.lesson_purchases.length.should eql 0
user.sales.length.should eql 0
lesson_session.amount_charged.should eql 0.0
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be true
lesson_session.sent_billing_notices.should be_nil
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.length.should eql 2 # one for student, one for teacher
UserMailer.deliveries.length.should eql 0 # one for student, one for teacher
LessonPaymentCharge.count.should eql 0
TeacherDistribution.count.should eql 0
end
it "affiliate gets their cut" do
user.affiliate_referral = affiliate_partner
user.save!
teacher_user.affiliate_referral = affiliate_partner2
teacher_user.save!
lesson = normal_lesson(user, teacher_user, true)
user.reload
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2
affiliate_partner.affiliate_distributions.count.should eql 1
affiliate_partner2.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.first
partner2_distribution = affiliate_partner2.affiliate_distributions.first
partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item
partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round
partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner2.lesson_rate).round
end
it "school affiliate gets nothing when teacher school is involved" do
teacher.school = school
teacher.save!
user.affiliate_referral = affiliate_partner
user.save!
lesson = normal_lesson(user, teacher_user, true)
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 1
affiliate_partner.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.first
partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round
school.affiliate_partner.affiliate_distributions.count.should eql 0
end
it "student school affiliates gets cut when student school is involved. so does teacher's" do
user.affiliate_referral = school.affiliate_partner
user.save!
teacher_user.affiliate_referral = affiliate_partner
teacher_user.save!
lesson = normal_lesson(user, teacher_user, true)
user.sales.count.should eql 1
user.sales[0].sale_line_items[0].affiliate_distributions.count.should eql 2
affiliate_partner.affiliate_distributions.count.should eql 1
partner1_distribution = affiliate_partner.affiliate_distributions.count.should eql 1
school.affiliate_partner.affiliate_distributions.count.should eql 1
school_partner_distribution = school.affiliate_partner.affiliate_distributions.first
school_partner_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * school.affiliate_partner.lesson_rate).round
end
end

View File

@ -13,7 +13,7 @@ describe "Recurring Lesson Flow" do
let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] }
let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] }
before(:each) do
after(:each) do
Timecop.return
end
it "works" do

View File

@ -12,12 +12,18 @@ describe "TestDrive Lesson Flow" do
let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) }
let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] }
let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] }
let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) }
let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) }
let(:school) {FactoryGirl.create(:school)}
before {
Timecop.return
teacher.stripe_account_id = stripe_account1_id
teacher.save!
}
after {
Timecop.return
}
it "works" do
@ -224,5 +230,9 @@ describe "TestDrive Lesson Flow" do
teacher_payment.teacher_payment_charge.amount_in_cents.should eql 1000
teacher_payment.teacher_payment_charge.fee_in_cents.should eql 0
user.sales.count.should eql 1
sale = user.sales[0]
sale.sale_line_items.count.should eql 1
sale.sale_line_items[0].affiliate_distributions.count.should eql 0
end
end

View File

@ -291,7 +291,7 @@ describe LessonBooking do
end
describe "billable_monthlies" do
before do
after do
Timecop.return
end

View File

@ -10,6 +10,7 @@ describe LessonSession do
let(:lesson_booking) {b = LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60); b.card_presumed_ok = true; b.save!; b}
let(:lesson_session) {lesson_booking.lesson_sessions[0]}
after{Timecop.return}
describe "accept" do
it "can accept" do
@ -18,18 +19,20 @@ describe LessonSession do
describe "upcoming_sessions_reminder" do
it "succeeds" do
lesson = normal_lesson(user, teacher)
UserMailer.deliveries.clear
LessonSession.upcoming_sessions_reminder
lesson_session.touch
lesson_session.sent_starting_notice.should be_false
lesson_session.is_requested?.should be_true
lesson_session.music_session.scheduled_start = 15.minutes.from_now
lesson_session.music_session.save!
#UserMailer.deliveries.count.should eql 2
lesson.touch
lesson.sent_starting_notice.should be_false
lesson.is_approved?.should be_true
lesson.music_session.scheduled_start = 15.minutes.from_now
lesson.music_session.save!
LessonSession.upcoming_sessions_reminder
UserMailer.deliveries.count.should eql 2
UserMailer.deliveries.clear
lesson_session.reload
lesson_session.sent_starting_notice.should be_true
lesson.reload
lesson.sent_starting_notice.should be_true
LessonSession.upcoming_sessions_reminder
UserMailer.deliveries.count.should eql 0
end

View File

@ -527,7 +527,6 @@ describe MusicSession do
dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd)
msess1 = FactoryGirl.create(:music_session, creator: creator, scheduled_start: dd)
Timecop.return
msess2 = FactoryGirl.create(:music_session, creator: creator)
music_sessions, user_scores = sms(searcher, default_opts)
expect(music_sessions.length).to be(1)
@ -921,7 +920,6 @@ describe MusicSession do
dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd)
msess1 = FactoryGirl.create(:music_session)
Timecop.return
msess2 = FactoryGirl.create(:music_session)
purging = MusicSession.purgeable_sessions
expect(purging.size).to be(1)
@ -934,7 +932,6 @@ describe MusicSession do
dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd)
msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now)
Timecop.return
msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now)
purging = MusicSession.purgeable_sessions
expect(purging.size).to be(1)

View File

@ -7,6 +7,9 @@ describe MusicSessionUserHistory do
let(:user_history1) { FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => music_session.creator) }
let(:user_history2) { FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => some_user) }
after {
Timecop.return
}
describe "create" do
pending
it {user_history1.music_session_id.should == music_session.id }
@ -31,7 +34,6 @@ describe MusicSessionUserHistory do
users = [user_history1, user_history2]
Timecop.travel(Time.now + (MusicSessionUserHistory::MIN_SESSION_DURATION_RATING * 1.5).seconds)
expect( user_history1.should_rate_session? ).to eq(true)
Timecop.return
end
it 'should rate fails' do

View File

@ -9,7 +9,7 @@ describe Sale do
let(:jam_track3) { FactoryGirl.create(:jam_track) }
let(:gift_card) { GiftCardType.jam_track_5 }
before(:each) {
after(:each) {
Timecop.return
}
def assert_free_line_item(sale_line_item, jamtrack)
@ -698,8 +698,8 @@ describe Sale do
partner1_distribution = affiliate_partner.affiliate_distributions.first
partner2_distribution = affiliate_partner2.affiliate_distributions.first
partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item
partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner.lesson_rate).round
partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner2.lesson_rate).round
partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner.lesson_rate).round
partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * 0.25 * affiliate_partner2.lesson_rate).round
end
it "book recurring, monthly" do

View File

@ -142,6 +142,7 @@ describe TeacherPayment do
end
it "charges school" do
teacher.touch
normal_distribution.school = school
normal_distribution.ready = true
normal_distribution.save!

View File

@ -1,4 +1,3 @@
module StripeMock
class ErrorQueue
def clear
@ -12,13 +11,13 @@ module StripeMock
end
end
def testdrive_lesson(user, teacher, slots = nil)
def testdrive_lesson(user, teacher, finish = false)
if slots.nil?
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_single)
slots << FactoryGirl.build(:lesson_booking_slot_single)
end
#if slots.nil?
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_single)
slots << FactoryGirl.build(:lesson_booking_slot_single)
#end
if user.stored_credit_card == false
user.stored_credit_card = true
@ -27,7 +26,10 @@ def testdrive_lesson(user, teacher, slots = nil)
booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.")
#puts "BOOKING #{booking.errors.inspect}"
if booking.errors.any?
puts "BOOKING #{booking.errors.inspect}"
end
booking.errors.any?.should be_false
lesson = booking.lesson_sessions[0]
booking.card_presumed_ok.should be_true
@ -42,21 +44,39 @@ def testdrive_lesson(user, teacher, slots = nil)
lesson.slot.should eql slots[0]
lesson.status.should eql LessonSession::STATUS_APPROVED
if finish
# teacher & student get into session
start = lesson.scheduled_start
end_time = lesson.scheduled_start + (60 * lesson.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson.music_session.session_removed_at = end_time
lesson.music_session.save!
Timecop.travel(end_time + 1)
lesson.analyse
lesson.session_completed
end
lesson
end
def normal_lesson(user, teacher, slots = nil)
def normal_lesson(user, teacher, finish = false)
if slots.nil?
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_single)
slots << FactoryGirl.build(:lesson_booking_slot_single)
end
#if slots.nil?
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_single)
slots << FactoryGirl.build(:lesson_booking_slot_single)
#end
if user.stored_credit_card == false
user.stored_credit_card = true
user.save!
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', normal: true})
#user.stored_credit_card = true
#user.save!
end
booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)
@ -76,21 +96,34 @@ def normal_lesson(user, teacher, slots = nil)
lesson.status.should eql LessonSession::STATUS_APPROVED
lesson.music_session.should_not be_nil
if finish
# teacher & student get into session
start = lesson.scheduled_start
end_time = lesson.scheduled_start + (60 * lesson.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson.music_session.session_removed_at = end_time
lesson.music_session.save!
Timecop.travel(end_time + 1)
lesson.analyse
lesson.session_completed
end
lesson
end
def monthly_lesson(user, teacher, slots = nil)
def monthly_lesson(user, teacher, finish = true)
if slots.nil?
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
end
slots = []
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
if user.stored_credit_card == false
user.stored_credit_card = true
user.save!
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', normal: true})
end
booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60)
@ -110,5 +143,29 @@ def monthly_lesson(user, teacher, slots = nil)
lesson.status.should eql LessonSession::STATUS_APPROVED
lesson.music_session.should_not be_nil
if finish
# teacher & student get into session
start = lesson.scheduled_start
end_time = lesson.scheduled_start + (60 * lesson.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson.music_session.session_removed_at = end_time
lesson.music_session.save!
Timecop.travel(end_time + 1)
lesson.analyse
lesson.session_completed
today = Date.today
if today.month == 12
next_month = Date.new(today.year + 1, 1, 1)
else
next_month = Date.new(today.year, today.month + 1, 1)
end
#Timecop.travel(next_month)
LessonBooking.hourly_check
end
lesson
end

View File

@ -92,9 +92,14 @@
$('.homecard.jamclass').on('click', function() {
context.JK.Banner.showNotice("Coming Soon!", "JamClass is just around the corner.")
if (gon.jamclass_enabled) {
return true;
}
else {
context.JK.Banner.showNotice("Coming Soon!", "JamClass is just around the corner.")
return false;
return false;
}
})
}

View File

@ -2480,6 +2480,16 @@
});
}
function createReview(options) {
return $.ajax({
type: "POST",
url: '/api/reviews',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options),
});
}
function initialize() {
return self;
@ -2703,6 +2713,7 @@
this.deleteSchoolStudent = deleteSchoolStudent;
this.listTeacherDistributions = listTeacherDistributions;
this.lessonStartTime = lessonStartTime;
this.createReview = createReview;
return this;
};

View File

@ -96,7 +96,8 @@ UserStore = context.UserStore
userDetailDone: (response) ->
if response.id == @state.teacherId
@setState({teacher: response, isSelf: response.id == context.JK.currentUserId})
school_on_school = response.teacher.school_id? && @state.user?.school_id? && response.teacher.school_id == @state.user.school_id
@setState({teacher: response, isSelf: response.id == context.JK.currentUserId, school_on_school: school_on_school})
else
logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId)
@ -226,7 +227,7 @@ UserStore = context.UserStore
booked: (response) ->
@setState({updating: false})
UserActions.refresh()
if response.user['has_stored_credit_card?']
if response.user['has_stored_credit_card?'] || @state.school_on_school
context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.<br/><br/>We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email.")
url = "/client#/jamclass/lesson-booking/#{response.id}"
url = "/client#/jamclass"

View File

@ -173,7 +173,7 @@ UserStore = context.UserStore
acceptLessonBookingFail: (response ) ->
@setState({updatingLesson: false})
logger.debug("accept lesson booking failed", response)
logger.debug("accept lesson booking failed " + response.responseText)
@app.ajaxError(arguments[0], arguments[1], arguments[2])
cancelLessonBookingFail: (jqXHR) ->
@ -699,12 +699,9 @@ UserStore = context.UserStore
renderTeacherRequested: () ->
console.log("renderTeacherRequested")
if @isTestDrive()
console.log("test drive")
action = `<p className="action">Has requested a TestDrive {this.lessonLength()}-minute lesson, for which you will be paid $10.</p>`
else
console.log("normal")
action = `<p className="action">Has requested a {this.lessonDesc()} lesson, for which you will be paid {this.lessonPaymentAmt()}.</p>`
`<div className="contents">

View File

@ -159,6 +159,7 @@ UserStore = context.UserStore
e.preventDefault()
if !window.Stripe?
logger.error("no window.Stripe")
@app.layout.notify({
title: 'Payment System Not Loaded',
text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
@ -230,6 +231,7 @@ UserStore = context.UserStore
@setState({updating: true})
logger.debug("creating stripe token")
window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
stripeResponseHandler: (status, response) ->
@ -269,6 +271,7 @@ UserStore = context.UserStore
data.name = @root.find('#set-user-on-card').val()
@setState({updating: true})
logger.debug("submitting purchase info: " + JSON.stringify(data))
rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
stripeSubmitted: (response) ->
@ -287,8 +290,6 @@ UserStore = context.UserStore
prefixLength = "test-drive-".length
packageLength = response.lesson_package_type.package_type.length
logger.debug("prefix: " + prefixLength.toString())
logger.debug("package: " + packageLength.toString())
testDriveCount = response.lesson_package_type.package_type.substring(prefixLength, packageLength)
logger.debug("testDriveCount: " + testDriveCount)
@ -336,6 +337,7 @@ UserStore = context.UserStore
window.location = "/client#/teachers/search"
stripeSubmitFailure: (jqXHR) ->
logger.debug("stripe submission failure", jqXHR.responseText)
@setState({updating: false})
handled = false
if jqXHR.status == 422

View File

@ -0,0 +1,103 @@
context = window
@RateUserDialog = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit")]
teacher: false
parseId:(id) ->
if !id?
{id: null, type: null}
else
bits = id.split('_')
if bits.length == 2
{id: bits[1], type: bits[0]}
else
{id: null, type: null}
beforeShow: (args) ->
logger.debug("RateUserDialog.beforeShow", args.d1)
@firstName = ''
@lastName = ''
@email = ''
@setState({target: null})
rest.getUserDetail({id: args.d1}).done((response) => @userLookupDone(response)).fail((jqXHR) => @userLookupFail(jqXHR))
afterHide: () ->
userLookupDone: (response) ->
@setState({target: response})
userLookupFail: (jqXHR) ->
@app.ajaxError(jqXHR, null, null)
onAppInit: (@app) ->
dialogBindings = {
'beforeShow': @beforeShow,
'afterHide': @afterHide
};
@app.bindDialog('rate-user', dialogBindings);
componentDidMount: () ->
@root = $(@getDOMNode())
getInitialState: () ->
{inviteErrors: null}
doCancel: (e) ->
e.preventDefault()
@app.layout.closeDialog('rate-user', true);
doRating: (e) ->
e.preventDefault()
rest.createReview({id: target})
createDone:(response) ->
context.SchoolActions.addInvitation(@state.teacher, response)
context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!")
@app.layout.closeDialog('invite-school-user')
createFail: (jqXHR) ->
handled = false
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
@setState({inviteErrors: errors})
handled = true
if !handled
@app.ajaxError(jqXHR, null, null)
render: () ->
if @state.user?.teacher?
title = 'Rate Teacher'
help = `<p>Please rate this teacher based on your experience with them.</p>`
else
title = 'Rate Student'
help = `<p>Please rate this student based on your experience with them.</p>`
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>{title}</h1>
</div>
<div className="dialog-inner">
{help}
<div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doRating} className="button-orange">SUBMIT RATING</a>
</div>
</div>
</div>`
})

View File

@ -11,7 +11,7 @@ LocationActions = @LocationActions
LIMIT: 20
getInitialState: () ->
{options: {}}
{options: {onlyMySchool: true}}
onAppInit: (@app) ->
@app.bindScreen('jamclass/searchOptions', {beforeShow: @beforeShow, afterShow: @afterShow})
@ -22,6 +22,7 @@ LocationActions = @LocationActions
afterShow: (e) ->
onUserChanged: (@user) ->
@setState(user: @user?.user)
onTeacherSearchChanged: (options) ->
@setState({options: options})
@ -59,12 +60,25 @@ LocationActions = @LocationActions
handleYearsTeaching: (e) ->
yearsTeaching = $(e.target).val()
logger.debug("years teaching:", yearsTeaching)
options = @state.options
options['years-teaching'] = yearsTeaching
@setState({options: options})
onlyMySchoolChange: (e) ->
$target = $(e.target)
checked = $target.is(':checked')
options = @state.options
options.onlyMySchool = checked
@setState(options)
render: () ->
if @state.user?.school_id?
onlySchoolOption =
`<div className="search-criteria school-options">
<h3>School Options</h3>
<input type="checkbox" name="onlyMySchool" className="onlyMySchool" checked={this.state.options.onlyMySchool} onChange={this.onlyMySchoolChange}/><label htmlFor="onlyMySchool">Only Show Teachers<br/>In My School</label>
</div>`
selectedAge = null
yearsTeaching = []
for yr in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 45, 50]
@ -129,6 +143,7 @@ LocationActions = @LocationActions
<div className="search-criteria">
<SelectLocation onItemChanged={this.handleLocationChange}/>
</div>
{onlySchoolOption}
</div>
<div className="actions">
<a className="button-grey" onClick={this.onCancel}>CANCEL</a>

View File

@ -48,6 +48,7 @@ ProfileActions = @ProfileActions
@needToSearch = false
afterHide: (e) ->
@contentBodyScroller.off('scroll')
onTeacherSearchStore: (storeChanged) ->
@needToSearch = true
@ -97,16 +98,17 @@ ProfileActions = @ProfileActions
))
if @state.next == null
@contentBodyScroller.off('scroll')
if @state.currentPage == 1 and @state.results.length == 0
@endOfList.text('No Teachers found matching your search').show()
logger.debug("TeacherSearch: empty search")
else if @state.currentPage > 0
logger.debug("end of search")
@endOfList.text('No more Teachers').show()
else
@registerInfiniteScroll(@contentBodyScroller)
if @visible
if @state.next == null
@contentBodyScroller.off('scroll')
if @state.currentPage == 1 and @state.results.length == 0
@endOfList.text('No Teachers found matching your search').show()
logger.debug("TeacherSearch: empty search")
else if @state.currentPage > 0
logger.debug("end of search")
@endOfList.text('No more Teachers').show()
else
@registerInfiniteScroll(@contentBodyScroller)
moreAboutTeacher: (user, e) ->
e.preventDefault()
@ -176,6 +178,10 @@ ProfileActions = @ProfileActions
searchOptions = TeacherSearchStore.getState()
summary = ''
if searchOptions.onlyMySchool
summary += "my school teachers only"
instruments = searchOptions.instruments
if instruments? && instruments.length > 0
if instruments.length == 1
@ -233,8 +239,12 @@ ProfileActions = @ProfileActions
if !bio?
bio = 'No bio'
console.log("@state.sur : #{@state.user.remaining_test_drives}, #{@state.user['can_buy_test_drive?']}")
if !@state.user? || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?']
school_on_school = user.teacher.school_id? && @state.user?.school_id? && user.teacher.school_id == @state.user.school_id
bookSingleBtn = null
bookTestDriveBtn = null
if !school_on_school && (!@state.user? || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?'])
bookTestDriveBtn = `<a className="button-orange try-test-drive" onClick={this.bookTestDrive.bind(this, user)}>BOOK TESTDRIVE LESSON</a>`
else
bookSingleBtn = `<a className="button-orange try-normal" onClick={this.bookNormalLesson.bind(this, user)}>BOOK LESSON</a>`

View File

@ -20,7 +20,6 @@ UserStore = context.UserStore
beforeHide: (e) ->
afterShow: (e) ->
logger.debug("TestDriveSelection: afterShow", e.id)
@ -60,7 +59,7 @@ UserStore = context.UserStore
packageSelect: (packageType, e) ->
e.preventDefault()
console.log("test-drive-#{packageType}")
logger.debug("user selected test-drive-#{packageType}")
rest.updateUser({desired_package: "test-drive-#{packageType}"}).done((response) => @packageSelectedDone(response)).fail((jqXHR) => @packageSelectedFail(jqXHR))
@ -72,7 +71,6 @@ UserStore = context.UserStore
url = "/client#/jamclass/book-lesson/test-drive"
console.log("TEACHER", JSON.stringify(@state.teacher))
if @state.teacher?
url += '_' + @state.teacher.id
else

View File

@ -76,6 +76,7 @@ TeacherSearchResultsActions = @TeacherSearchResultsActions
query.years_teaching = searchOptions['years-teaching']
query.country = searchOptions.location?.country
query.region = searchOptions.location?.region
query.onlyMySchool = searchOptions.onlyMySchool
query
query

View File

@ -8,7 +8,7 @@ TeacherSearchActions = @TeacherSearchActions
@TeacherSearchStore = Reflux.createStore(
{
listenables: TeacherSearchActions
searchOptions: {}
searchOptions: {onlyMySchool: true}
viewingTeacher: null
init: ->

View File

@ -86,6 +86,16 @@
margin-top:10px;
}
}
.school-options {
label {
display:inline-block;
vertical-align:middle;
}
input {
vertical-align: middle;
margin-right:10px;
}
}
.years-teaching-header {
margin-top:30px;

View File

@ -16,7 +16,6 @@ class ApiReviewsController < ApiController
# Create a review:
def create
@review = Review.create(params)
respond_with_model(@review)
end
# List reviews matching targets for given review summary:

View File

@ -72,6 +72,7 @@ module ClientHelper
gon.session_stat_thresholds = Rails.application.config.session_stat_thresholds
gon.midi_enabled = Rails.application.config.midi_enabled
gon.chat_blast = Rails.application.config.chat_blast
gon.jamclass_enabled = Rails.application.config.jamclass_enabled
# is this the native client or browser?
@nativeClient = is_native_client?

View File

@ -0,0 +1,3 @@
object @review
extends "api_reviews/show"

View File

@ -0,0 +1,3 @@
object @review
attributes :id, :rating

View File

@ -35,7 +35,8 @@ attributes :id,
:teaches_test_drive,
:test_drives_per_week,
:errors,
:profile_pct
:profile_pct,
:school_id
child :review_summary => :review_summary do
@ -107,3 +108,9 @@ node :experiences_award do |teacher|
}
end # collect
end
if current_user
node :has_rated_teacher do |teacher|
current_user.has_rated_teacher(teacher)
end
end

View File

@ -28,7 +28,7 @@ end
# give back more info if the user being fetched is yourself
if current_user && @user == current_user
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack, :remaining, :has_stored_credit_card?, :remaining_test_drives, :can_buy_test_drive?, :lesson_package_type_id
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack, :remaining, :has_stored_credit_card?, :remaining_test_drives, :can_buy_test_drive?, :lesson_package_type_id, :school_id
node :owned_school_id do |user|
user.owned_school.id if user.owned_school
@ -97,6 +97,9 @@ elsif current_user
node :internet_score do |user|
current_user.score_info(user)
end
node :has_rated_student do |user|
current_user.has_rated_student(user)
end
end
child :friends => :friends do

View File

@ -437,6 +437,6 @@ if defined?(Bundler)
config.lesson_wait_time_window_minutes = 10
config.end_of_wait_window_forgiveness_minutes = 1
config.olark_enabled = true
config.jamclass_enabled = false
end
end

View File

@ -8,9 +8,11 @@ describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => t
let(:teacher_user) {FactoryGirl.create(:teacher_user, first_name: "Teacher1", ready_for_session_at: Time.now)}
let(:teacher_user2) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)}
let(:teacher_user3) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)}
let(:school) {FactoryGirl.create(:school)}
before(:each) do
LessonPackagePurchase.destroy_all
LessonBooking.destroy_all
Recording.delete_all
Diagnostic.delete_all
@ -19,12 +21,8 @@ describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => t
UserMailer.deliveries.clear
emulate_client
# create an old test drive and fakely use up all the credits so that we can book the lesson
Timecop.travel(Date.new(2016, 03, 01))
testdrive_lesson(user, teacher_user3)
user.remaining_test_drives = 0
user.save!
user.reload
sign_in_poltergeist user
teacher_user.teacher.ready_for_session_at = Time.now
teacher_user.teacher.save!
teacher_user.teacher.price_per_lesson_60_cents.should eql 3000
@ -39,6 +37,14 @@ describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => t
end
it "succeeds" do
Timecop.travel(Date.new(2016, 03, 01))
testdrive_lesson(user, teacher_user3)
user.remaining_test_drives = 0
user.save!
user.reload
sign_in_poltergeist user
visit "/client#/teachers/search"
Timecop.travel(Date.new(2016, 04, 01))
@ -90,6 +96,7 @@ describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => t
lesson_booking = LessonBooking.where(teacher_id: teacher_user).order(:created_at).last
lesson_booking.should_not be_nil
lesson_booking.same_school.should be_false
lesson_session = LessonSession.where(teacher_id: teacher_user).order(:created_at).last
lesson_session.teacher.should eql teacher_user
@ -198,7 +205,177 @@ describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => t
lesson_session2.reload
lesson_session2.analysed.should be_true
analysis = JSON.parse(lesson_session2.analysis)
analysis["reason"].should eql LessonSessionAnalyser::NEITHER_SHOW
analysis["reason"].should eql LessonSessionAnalyser::TEACHER_FAULT
lesson_session2.billing_attempts.should eql 0
lesson_session2.billed.should eql false
lesson_session2.success.should be_false
lesson_session2.teacher_distribution.should be_nil
end
it "succeeds (school-on-school)" do
teacher_user.teacher.school = school
teacher_user.teacher.save
user.school = school
user.save!
sign_in_poltergeist user
visit "/client#/teachers/search"
Timecop.travel(Date.new(2016, 04, 01))
find('.teacher-search-result[data-teacher-id="' + teacher_user.id + '"] .try-normal').trigger(:click)
# book the lesson
fill_in "slot-1-date", with: "Sun Apr 17 2016"
#find('.slot.slot-1 input.hasDatepicker').trigger(:click)
# click 4-6
find('td a', text: '17').trigger(:click)
#find('.slot.slot-2 input.hasDatepicker').trigger(:click)
# click 4-7
fill_in "slot-2-date", with: "Mon Apr 18 2016"
find('td a', text: '18').trigger(:click)
fill_in 'user-description', with: 'abc def dog neck'
select('60 Minute Lesson for $30.00', :from => "booking-options-for-teacher")
sleep 3
find('a.book-lesson-btn', text: 'BOOK LESSON').trigger(:click)
#find('h2', text: 'your lesson has been requested')
#find('h2', text: 'enter payment info for lesson')
#fill_in 'card-number', with: '4111111111111111'
#fill_in 'expiration', with: '11/2016'
#fill_in 'cvv', with: '111'
#fill_in 'zip', with: '78759'
#find('.purchase-btn').trigger(:click)
# we tell user they have test drive purchased, and take them to the teacher screen
find('#banner h1', text: 'Lesson Requested')
# dismiss banner
find('a.button-orange', text:'CLOSE').trigger(:click)
user.student_lesson_bookings.count.should eql 1 # this single one
lesson_booking = user.student_lesson_bookings.order(:created_at).last
lesson_booking.is_requested?.should be_true
lesson_booking.card_presumed_ok.should be_false
lesson_booking.recurring.should be false
lesson_booking = LessonBooking.where(teacher_id: teacher_user).order(:created_at).last
lesson_booking.should_not be_nil
lesson_booking.school.should eql school
lesson_booking.same_school.should be_true
lesson_session = LessonSession.where(teacher_id: teacher_user).order(:created_at).last
lesson_session.teacher.should eql teacher_user
lesson_package_purchase = LessonPackagePurchase.where(user_id: user.id, teacher_id: teacher_user.id).order(:created_at).first
lesson_package_purchase.should be_nil
user.reload
user.remaining_test_drives.should eql 0
user.sales.count.should eql 0
# jamclass scren
find('h2', text: 'my lessons')
find('tr[data-lesson-session-id="' + lesson_session.id + '"] .displayStatusColumn', text: 'Requested')
find('tr[data-lesson-session-id="' + lesson_session.id + '"] .first_name', text: teacher_user.first_name)
# open up hover
find('tr[data-lesson-session-id="' + lesson_session.id + '"] .lesson-session-actions-btn').trigger(:click)
# check out the status
find('li[data-lesson-option="status"] a', text: 'View Status').trigger(:click)
# and now go to the session status screen
find('h2', text: 'your lesson has been requested')
# let's make a second request to a different teacher now
# let's make sure we can ask for another lesson too!
teacher_user2.teacher.ready_for_session_at = Time.now
teacher_user2.teacher.save!
visit "/client#/teachers/search"
find('span.search-summary', text: 'my school teachers only')
find('a.teacher-search-options').trigger(:click)
find('input.onlyMySchool').trigger(:click) # uncheck
find('a.search-btn').trigger(:click)
find('span.search-summary', text: 'all teachers')
find('.teacher-search-result[data-teacher-id="' + teacher_user2.id + '"] .try-test-drive').trigger(:click)
select_test_drive(4)
fill_out_single_lesson
fill_out_payment
sleep 5
find('h2', text: 'my lessons')
# dismiss banner
find('a.button-orange', text:'CLOSE').trigger(:click)
user.reload
user.has_stored_credit_card?.should be_true
lesson_session1 = lesson_session
lesson_booking = LessonBooking.where(teacher_id: teacher_user2.id).first
lesson_booking.should_not be_nil
lesson_booking.card_presumed_ok.should be_true
lesson_session = LessonSession.where(teacher_id: teacher_user2.id).first
lesson_session.teacher.should eql teacher_user2
lesson_session2 = lesson_session
lesson_package_purchase = LessonPackagePurchase.where(user_id: user.id, teacher_id: teacher_user.id).order(:created_at).first
lesson_package_purchase.should be_nil
user.reload
user.remaining_test_drives.should eql 3
user.sales.count.should eql 1
# approve by teacher:
teacher_approve(lesson_session1)
successful_lesson(lesson_session1)
LessonSession.hourly_check
lesson_session1.reload
lesson_session1.analysed.should be_true
analysis = JSON.parse(lesson_session1.analysis)
analysis["reason"].should eql LessonSessionAnalyser::SUCCESS
lesson_session1.success.should be_true
lesson_session1.billing_attempts.should eql nil
lesson_session1.billed.should eql false
LessonBooking.hourly_check
lesson_session1.reload
teacher_distribution = lesson_session1.teacher_distribution
teacher_distribution.should be_nil
# check the second session, which no one went too
lesson_session2.reload
lesson_session2.analysed.should be_false
# approve by teacher:
teacher_approve(lesson_session2)
LessonSession.hourly_check
LessonBooking.hourly_check
lesson_session2.reload
lesson_session2.analysed.should be_true
analysis = JSON.parse(lesson_session2.analysis)
analysis["reason"].should eql LessonSessionAnalyser::TEACHER_FAULT
lesson_session2.billing_attempts.should eql 0
lesson_session2.billed.should eql false
lesson_session2.success.should be_false

View File

@ -58,7 +58,7 @@ describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true
lesson_package_purchase = LessonPackagePurchase.where(user_id: user.id).first
lesson_package_purchase.should_not be_nil
lesson_package_purchase.lesson_package_type.is_test_drive?.should be_true
lesson_package_purchase.lesson_payment_charge.should_not be_nil
lesson_package_purchase.lesson_payment_charge.should be_nil
user.reload
user.remaining_test_drives.should eql 3
#lesson_package_purchase.amount_charged.should eql 49.99

View File

@ -46,6 +46,7 @@ describe "School Landing", :js => true, :type => :feature, :capybara_feature =>
user.is_a_teacher.should be false
user.school_interest.should be true
user.owned_school.should_not be_nil
user.owned_school.affiliate_partner.should_not be_nil
user.musician.should be true
end

View File

@ -1,4 +1,4 @@
require 'spec_helper'
require 'spec_helper'
describe "Student Landing", :js => true, :type => :feature, :capybara_feature => true do
@ -52,6 +52,7 @@ describe "Student Landing", :js => true, :type => :feature, :capybara_feature =>
student.musician.should be true
student.teacher.should be_nil
student.school.should eql school
student.affiliate_referral.should eql school.affiliate_partner
find('#user-profile #username', text: student.name)
@ -87,6 +88,7 @@ describe "Student Landing", :js => true, :type => :feature, :capybara_feature =>
student.musician.should be true
student.teacher.should be_nil
student.school.should eql school
student.affiliate_referral.should eql school.affiliate_partner
school_invitation.reload
school_invitation.accepted.should be_true
@ -109,6 +111,7 @@ describe "Student Landing", :js => true, :type => :feature, :capybara_feature =>
find('#user-profile #username', text: user.name)
user.reload
user.affiliate_referral.should be_nil
#user.is_a_student.should be true
#user.is_a_teacher.should be false
#user.musician.should be true

View File

@ -50,6 +50,7 @@ describe "Teacher Landing", :js => true, :type => :feature, :capybara_feature =>
teacher.teacher.should_not be_nil
teacher.school.should be_nil
teacher.teacher.school.should eql school
teacher.affiliate_referral.should eql school.affiliate_partner
find('#user-profile #username', text: teacher.name)
UserMailer.deliveries.count.should eql 2
@ -85,6 +86,7 @@ describe "Teacher Landing", :js => true, :type => :feature, :capybara_feature =>
teacher.teacher.should_not be_nil
teacher.school.should be_nil
teacher.teacher.school.should eql school
teacher.affiliate_referral.should eql school.affiliate_partner
school_invitation.reload
school_invitation.accepted.should be_true
@ -107,6 +109,7 @@ describe "Teacher Landing", :js => true, :type => :feature, :capybara_feature =>
find('#user-profile #username', text: user.name)
user.reload
user.affiliate_referral.should be_nil
#user.is_a_student.should be true
#user.is_a_teacher.should be false
#user.musician.should be true

View File

@ -63,6 +63,9 @@ def fill_out_payment
fill_in 'zip', with: '78759'
find('.purchase-btn').trigger(:click)
# it needs to go 'disabled state' to indicate it's updating
find('.purchase-btn.disabled')
end
def select_test_drive(count = 4)