diff --git a/admin/app/admin/gift_card_upload.rb b/admin/app/admin/gift_card_upload.rb index 87e69850e..3165e22b0 100644 --- a/admin/app/admin/gift_card_upload.rb +++ b/admin/app/admin/gift_card_upload.rb @@ -28,7 +28,7 @@ ActiveAdmin.register_page "Giftcarduploads" do end content do - semantic_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f| + active_admin_form_for GiftCard.new, :url => admin_giftcarduploads_upload_giftcards_path, :builder => ActiveAdmin::FormBuilder do |f| f.inputs "Upload Gift Cards" do f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, etc)" f.input :card_type, required:true, as: :select, :collection => JamRuby::GiftCard::CARD_TYPES diff --git a/admin/app/admin/interested_education.rb b/admin/app/admin/interested_education.rb new file mode 100644 index 000000000..cdd9eede0 --- /dev/null +++ b/admin/app/admin/interested_education.rb @@ -0,0 +1,20 @@ +ActiveAdmin.register JamRuby::User, :as => 'EducationInterest' do + + menu :label => 'Interested in Education', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.where(education_interest: true) } + + index do + column "Name" do |user| + span do + link_to "#{user.name} (#{user.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/interested_retailers.rb b/admin/app/admin/interested_retailers.rb new file mode 100644 index 000000000..1d184807f --- /dev/null +++ b/admin/app/admin/interested_retailers.rb @@ -0,0 +1,20 @@ +ActiveAdmin.register JamRuby::User, :as => 'RetailerInterest' do + + menu :label => 'Interested in Retailers', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.where(retailer_interest: true) } + + index do + column "Name" do |user| + span do + link_to "#{user.name} (#{user.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/monthly_stats.rb b/admin/app/admin/monthly_stats.rb index 0a8e609bb..24e394a93 100644 --- a/admin/app/admin/monthly_stats.rb +++ b/admin/app/admin/monthly_stats.rb @@ -5,31 +5,31 @@ ActiveAdmin.register_page "Monthly Stats" do content :title => "Monthly Stats" do h2 "Distinct Users Playing in Sessions" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', msuh.created_at)::date as month, count(distinct(user_id)) from music_sessions_user_history msuh group by month order by month desc;") do - column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Month", Proc.new { |row| row.month.strftime('%B %Y') } column "Users", :count end h2 "Music Sessions" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', ms.created_at)::date as month, count(id) from music_sessions ms where started_at is not null group by month order by month desc;") do - column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Month", Proc.new { |row| row.month.strftime('%B %Y') } column "Sessions", :count end h2 "Distinct Users Who Played with a JamTrack" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(user_id)) from jam_track_sessions jts group by month order by month desc;") do - column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Month", Proc.new { |row| row.month.strftime('%B %Y') } column "Users", :count end h2 "Music Sessions with JamTracks Played" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(music_session_id)) from jam_track_sessions jts where session_type = 'session' group by month order by month desc;") do - column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Month", Proc.new { |row| row.month.strftime('%B %Y') } column "Sessions", :count end h2 "JamTrack Web Player Sessions" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(id) from jam_track_sessions jts where session_type = 'browser' group by month order by month desc;") do - column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } + column "Month", Proc.new { |row| row.month.strftime('%B %Y') } column "Sessions", :count end diff --git a/admin/app/admin/posa_card_upload.rb b/admin/app/admin/posa_card_upload.rb new file mode 100644 index 000000000..75353ca6a --- /dev/null +++ b/admin/app/admin/posa_card_upload.rb @@ -0,0 +1,58 @@ +ActiveAdmin.register_page "POSA Card Uploads" do + + menu :label => 'Posa Cards Upload', :parent => 'JamClass' + + page_action :upload_posacards, :method => :post do + PosaCard.transaction do + + puts params + + file = params[:jam_ruby_posa_card][:csv] + array_of_arrays = CSV.read(file.tempfile.path) + array_of_arrays.each do |row| + if row.length != 1 + raise "UKNONWN CSV FORMAT! Must be 1 column" + end + + code = row[0] + + posa_card = PosaCard.new + posa_card.code = code + posa_card.card_type = params[:jam_ruby_posa_card][:card_type] + posa_card.origin = file .original_filename + posa_card.save! + end + + redirect_to admin_posa_card_uploads_path, :notice => "Created #{array_of_arrays.length} POSA cards!" + end + end + +=begin + form :html => {:multipart => true} do |f| + f.inputs "Details" do + f.input :version, :hint => "Should match Jenkins build number of artifact" + f.input :environment, :hint => "Typically just 'public'" + f.input :product, :as => :select, :collection => JamRuby::ArtifactUpdate::PRODUCTS + end + f.inputs "Artifact Upload" do + f.input :uri, :as => :file, :hint => "Upload the artifact from Jenkins" + end + + f.actions + + end +=end + + content do + active_admin_form_for PosaCard.new, :url => admin_posa_card_uploads_upload_posacards_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "Upload POSA Cards" do + f.input :csv, as: :file, required: true, :label => "A single column CSV that contains ONE type of gift card (5 JamTrack, 10 JamTrack, 4 JamClass etc)" + f.input :card_type, required:true, as: :select, :collection => JamRuby::PosaCard::CARD_TYPES + + end + f.actions + end + end + +end + diff --git a/admin/app/admin/sale_line_items.rb b/admin/app/admin/sale_line_items.rb index fa90ffc4e..c64862611 100644 --- a/admin/app/admin/sale_line_items.rb +++ b/admin/app/admin/sale_line_items.rb @@ -25,7 +25,9 @@ ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do link_to("#{oo.affiliate_referral.display_name} #{oo.affiliate_referral_fee_in_cents ? "#{oo.affiliate_referral_fee_in_cents}\u00A2" : ''}", oo.affiliate_referral.admin_url, {:title => oo.affiliate_referral.display_name}) if oo.affiliate_referral end column 'User' do |oo| - link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name}) + if oo.sale.user + link_to(oo.sale.user.name, admin_user_path(oo.sale.user.id), {:title => oo.sale.user.name}) + end end column 'Source' do |oo| oo.sale.source diff --git a/admin/app/controllers/artifacts_controller.rb b/admin/app/controllers/artifacts_controller.rb index f9ee76b4b..88699cd80 100644 --- a/admin/app/controllers/artifacts_controller.rb +++ b/admin/app/controllers/artifacts_controller.rb @@ -14,7 +14,7 @@ class ArtifactsController < ApplicationController ArtifactUpdate.transaction do # VRFS-1071: Postpone client update notification until installer is available for download ArtifactUpdate.connection.execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED') - @artifact = ArtifactUpdate.find_or_create_by({product: product, environement: environment}) + @artifact = ArtifactUpdate.find_or_create_by({product: product, environment: environment}) @artifact.version = version @artifact.uri = file diff --git a/db/manifest b/db/manifest index 66680d0e7..5a9ba3bbd 100755 --- a/db/manifest +++ b/db/manifest @@ -363,4 +363,9 @@ jamblasters_network.sql immediate_recordings.sql nullable_user_id_jamblaster.sql rails4_migration.sql -non_free_jamtracks.sql \ No newline at end of file +non_free_jamtracks.sql +retailers.sql +second_ed.sql +second_ed_v2.sql +retailers_v2.sql +retailer_interest.sql \ No newline at end of file diff --git a/db/up/retailer_interest.sql b/db/up/retailer_interest.sql new file mode 100644 index 000000000..7be2a8290 --- /dev/null +++ b/db/up/retailer_interest.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN retailer_interest BOOLEAN DEFAULT FALSE NOT NULL; +alter table retailers alter column slug drop not null; \ No newline at end of file diff --git a/db/up/retailers.sql b/db/up/retailers.sql new file mode 100644 index 000000000..0a3d564b7 --- /dev/null +++ b/db/up/retailers.sql @@ -0,0 +1,96 @@ +CREATE TABLE retailers ( + id INTEGER PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + name VARCHAR, + enabled BOOLEAN DEFAULT TRUE, + city VARCHAR, + state VARCHAR, + slug VARCHAR NOT NULL, + encrypted_password VARCHAR NOT NULL DEFAULT uuid_generate_v4(), + photo_url VARCHAR(2048), + original_fpfile VARCHAR(8000), + cropped_fpfile VARCHAR(8000), + cropped_s3_path VARCHAR(8000), + crop_selection VARCHAR(256), + large_photo_url VARCHAR(512), + cropped_large_s3_path VARCHAR(512), + cropped_large_fpfile VARCHAR(8000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE SEQUENCE retailer_key_sequence; +ALTER SEQUENCE retailer_key_sequence RESTART WITH 10000; +ALTER TABLE retailers ALTER COLUMN id SET DEFAULT nextval('retailer_key_sequence'); + + +CREATE TABLE retailer_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + retailer_id INTEGER REFERENCES retailers(id) NOT NULL, + invitation_code VARCHAR(256) NOT NULL UNIQUE, + note VARCHAR, + email VARCHAR NOT NULL, + first_name VARCHAR, + last_name VARCHAR, + accepted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE posa_cards ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + code VARCHAR(64) UNIQUE NOT NULL, + user_id VARCHAR (64) REFERENCES users(id) ON DELETE SET NULL, + card_type VARCHAR(64) NOT NULL, + origin VARCHAR(200), + activated_at TIMESTAMP, + claimed_at TIMESTAMP, + retailer_id INTEGER REFERENCES retailers(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX posa_card_user_id_idx ON posa_cards(user_id); + +ALTER TABLE users ADD COLUMN jamclass_credits INTEGER DEFAULT 0; + + +CREATE TABLE posa_card_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + card_type VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5'); +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10'); +INSERT INTO posa_card_types (id, card_type) VALUES ('jam_class_10', 'jam_class_10'); + +CREATE TABLE posa_card_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL, + posa_card_type_id VARCHAR(64) REFERENCES posa_card_types(id) ON DELETE SET NULL, + posa_card_id VARCHAR(64) REFERENCES posa_cards(id) ON DELETE SET NULL, + recurly_adjustment_uuid VARCHAR(500), + recurly_adjustment_credit_uuid VARCHAR(500), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE sale_line_items ADD COLUMN posa_card_purchase_id VARCHAR(64) REFERENCES posa_card_purchases(id); + + +ALTER TABLE teachers ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teachers ADD COLUMN joined_retailer_at TIMESTAMP; +ALTER TABLE retailers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; +ALTER TABLE retailers ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id); +ALTER TABLE lesson_bookings ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teacher_payments ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE teacher_distributions ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); + + +ALTER TABLE sales ALTER COLUMN user_id DROP NOT NULL; +ALTER TABLE sales ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); +ALTER TABLE sale_line_items ADD COLUMN retailer_id INTEGER REFERENCES retailers(id); \ No newline at end of file diff --git a/db/up/retailers_v2.sql b/db/up/retailers_v2.sql new file mode 100644 index 000000000..73f5ea21d --- /dev/null +++ b/db/up/retailers_v2.sql @@ -0,0 +1,3 @@ +ALTER TABLE lesson_bookings ADD COLUMN posa_card_id VARCHAR(64); +ALTER TABLE jam_track_rights ADD COLUMN posa_card_id VARCHAR(64); +ALTER TABLE lesson_package_purchases ADD COLUMN posa_card_id VARCHAR(64); \ No newline at end of file diff --git a/db/up/second_ed.sql b/db/up/second_ed.sql new file mode 100644 index 000000000..f89ce3014 --- /dev/null +++ b/db/up/second_ed.sql @@ -0,0 +1,4 @@ +ALTER TABLE schools ADD COLUMN education BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE teacher_distributions ADD COLUMN education BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE lesson_bookings ADD COLUMN same_school_free BOOLEAN NOT NULL DEFAULT FALSE; +UPDATE lesson_bookings SET same_school_free = true where same_school = true; \ No newline at end of file diff --git a/db/up/second_ed_v2.sql b/db/up/second_ed_v2.sql new file mode 100644 index 000000000..a520116e9 --- /dev/null +++ b/db/up/second_ed_v2.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN education_interest BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 45fc7b857..4afe09eb5 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -280,6 +280,9 @@ require "jam_ruby/models/subject" require "jam_ruby/models/band_search" require "jam_ruby/import/tency_stem_mapping" require "jam_ruby/models/jam_track_search" +require "jam_ruby/models/posa_card" +require "jam_ruby/models/posa_card_type" +require "jam_ruby/models/posa_card_purchase" require "jam_ruby/models/gift_card" require "jam_ruby/models/gift_card_purchase" require "jam_ruby/models/gift_card_type" @@ -305,6 +308,8 @@ require "jam_ruby/models/affiliate_distribution" require "jam_ruby/models/teacher_intent" require "jam_ruby/models/school" require "jam_ruby/models/school_invitation" +require "jam_ruby/models/retailer" +require "jam_ruby/models/retailer_invitation" require "jam_ruby/models/teacher_instrument" require "jam_ruby/models/teacher_subject" require "jam_ruby/models/teacher_language" diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index aab962c1f..ee7229a6a 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -49,9 +49,27 @@ module JamRuby end end + def student_education_welcome_message(user) + @user = user + @subject = "Welcome to JamKazam and JamClass online lessons!" + @education = user.school && user.school.education + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + def student_welcome_message(user) @user = user @subject = "Welcome to JamKazam and JamClass online lessons!" + @education = user.school && user.school.education sendgrid_category "Welcome" sendgrid_unique_args :type => "welcome_message" @@ -68,6 +86,9 @@ module JamRuby def teacher_welcome_message(user) @user = user @subject= "Welcome to JamKazam and JamClass online lessons!" + + @education = user.teacher && user.teacher.school && user.teacher.school.education + sendgrid_category "Welcome" sendgrid_unique_args :type => "welcome_message" @@ -97,6 +118,38 @@ module JamRuby end end + def retailer_owner_welcome_message(user) + @user = user + @subject= "Welcome to JamKazam!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + + def education_owner_welcome_message(user) + @user = user + @subject= "Welcome to JamKazam and JamClass online lessons!" + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => @subject) do |format| + format.text + format.html + end + end + def password_changed(user) @user = user @@ -1002,7 +1055,20 @@ module JamRuby @user = lesson_session.student email = @student.email - subject = "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits" + + + if lesson_session.posa_card + @total_credits = @student.total_posa_credits + @used_credits = @student.used_posa_credits + @remaining_credits = @student.jamclass_credits + else + @total_credits = @student.total_test_drives + @used_credits = @student.used_test_drives + @remaining_credits = @student.remaining_test_drives + end + + subject = "You have used #{@used_credits} of #{@total_credits} TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} sendgrid_category "Notification" @@ -1698,6 +1764,26 @@ module JamRuby end end + def invite_retailer_teacher(retailer_invitations) + @retailer_invitation = retailer_invitations + @retailer = retailer_invitations.retailer + + email = retailer_invitations.email + @subject = "#{@retailer.owner.name} has sent you an invitation to join #{@retailer.name} on JamKazam" + unique_args = {:type => "invite_retailer_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + def invite_school_teacher(school_invitation) @school_invitation = school_invitation @school = school_invitation.school @@ -1882,7 +1968,22 @@ module JamRuby format.text format.html { render :layout => "from_user_mailer" } end + end + def retailer_customer_blast(email, retailer) + @retailer = retailer + @subject = "Check out our teachers at #{@retailer.name}" + unique_args = {:type => "retailer_customer_email"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + 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/education_owner_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/education_owner_welcome_message.html.erb new file mode 100644 index 000000000..eadbf65a5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/education_owner_welcome_message.html.erb @@ -0,0 +1,36 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +

+ Thank you for expressing an interest in exploring our secondary education partner program! A member of our staff will + reach out to you shortly to chat with you and answer any questions you have about our partner program, our + technologies, and how we can help you continue to deliver the best possible music education to your students. +

+ +

+ It takes less than 1 hour of your time to set up your music program to partner with JamKazam. And we are happy to walk + you through the process step by step, so you don't have to worry about figuring out how to do this. But if you're + curious, then you can check out our + help + articles for music program directors. These help articles explain things from + the perspective of the school program director - e.g. how to set up your school, how to invite teachers and students + to sign up if they wish, how distributions are made into your booster fund, and so on. +

+ + +

+ JamKazam handles all the technical support needed to help your students, as well as any preferred teachers associated + with your music program, to get set up and ready to go. We even get into a sample online session with each individual + to make sure everything is working, and to show them around the features they'll use in online lessons. But if you are + curious about how it all works, you can also review our help guide for students and our help guide for teachers. +

+

+ Thanks again for connecting with us, and we look forward to speaking with you soon! +

+

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/education_owner_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/education_owner_welcome_message.text.erb new file mode 100644 index 000000000..b101fb88e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/education_owner_welcome_message.text.erb @@ -0,0 +1,21 @@ +<% if !@user.anonymous? %> + Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> +<% end %> + +Thank you for expressing an interest in exploring our secondary education partner program! A member of our staff will +reach out to you shortly to chat with you and answer any questions you have about our partner program, our +technologies, and how we can help you continue to deliver the best possible music education to your students. + +It takes less than 1 hour of your time to set up your music program to partner with JamKazam. And we are happy to walk +you through the process step by step, so you don't have to worry about figuring out how to do this. But if you're +curious, then you can check out our help articles for music program directors -- https://jamkazam.desk.com/customer/en/portal/topics/985544-jamclass-online-music-lessons---for-secondary-education-music-program-directors/articles. These help articles explain things from +the perspective of the school program director - e.g. how to set up your school, how to invite teachers and students +to sign up if they wish, how distributions are made into your booster fund, and so on. + +JamKazam handles all the technical support needed to help your students, as well as any preferred teachers associated +with your music program, to get set up and ready to go. We even get into a sample online session with each individual +to make sure everything is working, and to show them around the features they'll use in online lessons. But if you are +curious about how it all works, you can also review our help guide for students -- https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles and our help + guide for teachers -- https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles. +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb new file mode 100644 index 000000000..f097b022e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_student.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +

<%= @retailer.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can + register to take online music lessons with <%= @retailer.name %>. To accept this invitation, please click the SIGN UP NOW + button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of + <%= @retailer.name %>, welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb new file mode 100644 index 000000000..25c0d5ef4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +
+

+ <%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music + lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below, + and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!

+
+

+ SIGN + UP NOW +

+ +
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb new file mode 100644 index 000000000..f4b5bffe9 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_retailer_teacher.text.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +Hello <%= @retailer_invitation.first_name %> - +<%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music +lessons in an amazing new way that really works. To accept this invitation, please click the link below, +and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam! + +<%= @retailer_invitation.generate_signup_url %> + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb new file mode 100644 index 000000000..a98071d65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.html.erb @@ -0,0 +1,8 @@ +<% provide(:title, @subject) %> + +

Click the link of each teacher's profile at <%= @retailer.name %> to find the best fit for you:

+ \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb new file mode 100644 index 000000000..4c59f45a4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_customer_blast.text.erb @@ -0,0 +1,6 @@ + +Check out each teacher's profile at <%= @retailer.name %> to find the best fit for you: + +<% @retailer.teachers.each do |teacher| %> +<%= teacher.user.name %>: <%= teacher.user.teacher_profile_url%> (<%= teacher.teaches %>) +<% end %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.html.erb new file mode 100644 index 000000000..bd71e888a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +

+ Thank you for expressing an interest in exploring our retailer partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program and our technologies. +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.text.erb new file mode 100644 index 000000000..4a13e9d0a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/retailer_owner_welcome_message.text.erb @@ -0,0 +1,8 @@ +<% if !@user.anonymous? %> +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> +<% end %> + +Thank you for expressing an interest in exploring our retailer partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program and our technologies. + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.html.erb new file mode 100644 index 000000000..10426f858 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.html.erb @@ -0,0 +1,80 @@ +<% provide(:title, @subject) %> + + +<% if !@user.anonymous? %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+<% end %> + +<% if @education %> +

+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was built from the ground up for playing music live in sync with studio quality audio from different locations over the Internet, and for delivering amazing online music lessons. +

+

+ To get ready to take JamClass lessons online, here are the things you'll want to do: +

+ +

1. Set Up Your Gear
+ When you sign up, someone from JamKazam will get in touch with you via email within a couple of business days to help you get set up. If you don't hear from us within a couple of days, please email us at support@jamkazam.com or call us at 1-877-376-8742. To play in online lessons, you will need at a minimum: (1) a Windows or Mac computer; (2) normal home Internet service; and (3) a pair of headphones or earbuds you can plug into the headphone minijack on your computer. If you would like to benefit from studio quality audio (recommended) in your lessons, JamKazam offers an amazing audio package for just $49.99 (less than our cost) that includes an audio interface (a little box that connects to your computer via USB cable), a microphone, a mic cable, and a mic stand. We'll discuss these options with you, and we're happy to support you whichever path you choose. We'll help step you through the setup process, and we'll even get into an online session with you to make sure everything is working properly, and to show you some of the key features you can use during online lessons. + +

+ +

2. Book Lessons
+ Once your gear is set up, you are ready to take lessons. Go to this web page: <%= @user.school.teacher_list_url %>. If your school has preferred instructors, they will be listed on this page, and you can click a button to book a lesson with the teacher from whom you want to take lessons. If your school doesn't have preferred instructors, then there is a link on this page to use our teacher search feature to find a great instructor from our broader community of teachers. You'll need your parents to enter credit card information to pay for your lessons. We use one of the largest and most secure commerce platforms on the Internet called Stripe, so you can feel confident your financial information will be very secure. +

+ +

3. Learn About JamClass Features
+ You can also review our JamClass user guide for students + to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to join your teacher in online lessons, features you can use while in lessons, and more. +

+ +

+ Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician! +

+<% else %> + + +

+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was + built from the ground up for playing music live in sync with high quality audio from different locations over the + Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience + than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this. +

+ +

+ To get ready to take JamClass lessons online, here are the things you'll want to do: +

+ +

1. Find a Teacher & Book Lessons
+ + If you haven't done so already, use this link to search our teachers, and click to book a TestDrive with a teacher who looks good for you. When you do this, you'll be given the option to take full 30-minute TestDrive lessons: + +

+

+ Pick whichever option you prefer. TestDrive lets you safely and easily try multiple teachers to find the one who is best specifically for you, which is a great way to maximize the benefit from your lessons. And TestDrive lessons are heavily discounted to give you a risk-free way to get started. We'd suggest scheduling your first lesson for about a week in the future to give you plenty of time to get up and running with our free app. +

+

+ +

2. Set Up Your Gear
+ Please review our help articles on gear recommendations + to make sure you have everything you need to get set up properly for best results in your online lessons. + If you have everything you need, then you can follow the instructions on our setup help articles to download and install our free app and set it up with your audio gear and webcam. Please email us at support@jamkazam.com or call us at 1-877-376-8742 any time so that we can help you with these steps. We are very happy to help, and we also strongly suggest that you let one of our staff get into an online session with you to make sure everything is working properly and to make sure you're comfortable with the app and ready for your first lesson. +

+ +

3. Learn About JamClass Features
+ Please review our JamClass user guide for students to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to search for the best teacher for you, how to request/book lessons, how to join your teacher in online lessons, features you can use while in lessons, and much more. +

+ +

+ + Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician! +

+ +<% end %> + +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.text.erb new file mode 100644 index 000000000..b16eef375 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_education_welcome_message.text.erb @@ -0,0 +1,57 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was +built from the ground up for playing music live in sync with high quality audio from different locations over the +Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience +than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this. + + +To get ready to take JamClass lessons online, here are the things you'll want to do: + +1. Find a Teacher & Book Lessons +If you already know the teacher from whom you want to learn, then you can simply +use this link to search for them (https://www.jamkazam.com/client#/jamclass/searchOptions), and +click the Book Lesson button to get started. But if you're like most of us, you don't know. In this case, we strongly +advise signing up for our unique TestDrive service. + +TestDrive lets you take 4 full lessons (30 minutes each) from 4 different teachers for just $49.99 to find the best +teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would +you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click +with. It's a phenomenal value, and then you can stick with the best teacher for you. +Click this link to sign up now for TestDrive (https://www.jamkazam.com/client#/jamclass/test-drive-selection). +Then you can book 4 TestDrive lessons to get rolling. + +2. Set Up Your Gear +Use this link to a set of +help articles on how to set up your gear (https://jamkazam.desk.com/customer/en/portal/topics/673197-first-time-setup/articles) +to be ready to teach online. After you have signed +up, someone from JamKazam will contact you to schedule a test online session, in which we will make sure your audio +and video gear are working properly in an online session, and to make sure you feel comfortable with the key features +you will be using in sessions with teachers. + +3. Learn About JamClass Features +Use this link to a set of help articles for students on JamClass (https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles) +to familiarize yourself with the most useful features +for online lessons. This includes how to search for the best teacher for you, how to request/book lessons, how to join +your teacher in online lessons, features you can use while in lessons, and much more. There is very important basic +information, plus some really nifty stuff here, so be sure to look through it at least briefly to see how we can +turbocharge your online lessons! + +4. Play With Other Musicians Online - It's Free! +With JamKazam, you can use the things you're learning in lessons to play with other amateur musicians in online +sessions, free! Or just play for fun. Once you've set up your gear for lessons, you can +create online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599977-creating-a-session) +that others can join, or find other musicians' online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599978-finding-a-session) +and hop into those to play with others. If you +want to take advantage of this part of the JamKazam platform, we'd advise that you edit your musician profile (https://www.jamkazam.com/client#/account/profile) to make +it easier to connect with other musicians (https://jamkazam.desk.com/customer/en/portal/articles/1707418-connecting-with-other-musicians) in our community to expand your set of musician friends. It's a ton of fun, +so give it a try! + +As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for +help. You can email us any time at support@jamkazam.com. We are happy to +help you, and we look forward to helping you +learn and grow as a musician, and expand your musical universe! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb index 56e8750e5..8323ff836 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -1,4 +1,4 @@ -<% provide(:title, "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %> +<% provide(:title, "You have used #{@used_credits} of #{@total_credits} TestDrive lesson credits") %> <% provide(:photo_url, @teacher.resolved_photo_url) %> <% content_for :note do %> @@ -7,7 +7,7 @@

We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have - used <%= @student.used_test_drives %> TestDrive credits, and you have <%= @student.remaining_test_drives %> + used <%= @used_credits %> TestDrive credits, and you have <%= @remaining_credits %> remaining TestDrive lesson(s) available. If you haven’t booked your next TestDrive lesson, click here to search our teachers and get your next lesson lined up today!

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb index 34db7e7bc..320ae8fc1 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -1,4 +1,4 @@ -You have used <%= @student.used_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits. +You have used <%= @used_credits %> of <%= @total_credits %> TestDrive lesson credits. <% if @student.has_rated_teacher(@teacher) %> Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb index d2d9a47f2..ddbe24f42 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -6,21 +6,62 @@

<% end %> +<% if @education %> -

- Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was - built from the ground up for playing music live in sync with high quality audio from different locations over the - Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience - than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this. -

+

+ Thank you for expressing an interest in taking private music lessons online using JamKazam! A member of our staff + will reach out to you shortly to chat with you and answer any questions you have about how our online music lesson + service works, to help you determine if this is a good option for you. +

+

+ If you decide online lessons look good, then we'll help you get your gear set up properly, and we'll even get into a + sample online session with you to make sure everything is working properly, and to ensure you are comfortable using + our app's features in a lesson. +

-

- To get ready to take JamClass lessons online, here are the things you'll want to do: -

+

To take online lessons on JamKazam, you'll need the following things at home:

-

1. Find a Teacher & Book Lessons
+

- If you haven't done so already, use this link to search our teachers, and click to book a TestDrive with a teacher who looks good for you. When you do this, you'll be given the option to take full 30-minute TestDrive lessons: +

+ For higher quality audio in online sessions, we recommend (this is an option, not a requirement) a premium audio + gear bundle that includes an audio interface (a little box you connect to your computer with a USB cable), a + microphone, a mic cable, and a mic stand. We offer this package for just $49.99, which is less than our cost for + these products, but it makes a big difference in audio quality, so we think it's worth the upgrade. +

+ +

+ If you are curious to learn more about how everything works, you can also review our + help + guide for students. Thanks + again for connecting with us, and we look forward to speaking with you soon! +

+ +<% else %> + + +

+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology + was + built from the ground up for playing music live in sync with high quality audio from different locations over the + Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience + than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians + will attest to this. +

+ +

+ To get ready to take JamClass lessons online, here are the things you'll want to do: +

+ +

1. Find a Teacher & Book Lessons
+ + If you haven't done so already, + use this link to search our + teachers, and click to book a TestDrive with a teacher who looks good for you. When you do this, you'll be + given the option to take full 30-minute TestDrive lessons:

- Pick whichever option you prefer. TestDrive lets you safely and easily try multiple teachers to find the one who is best specifically for you, which is a great way to maximize the benefit from your lessons. And TestDrive lessons are heavily discounted to give you a risk-free way to get started. We'd suggest scheduling your first lesson for about a week in the future to give you plenty of time to get up and running with our free app. + Pick whichever option you prefer. TestDrive lets you safely and easily try multiple teachers to find the one who is + best specifically for you, which is a great way to maximize the benefit from your lessons. And TestDrive lessons are + heavily discounted to give you a risk-free way to get started. We'd suggest scheduling your first lesson for about a + week in the future to give you plenty of time to get up and running with our free app. +

-

-

2. Set Up Your Gear
- Please review our help articles on gear recommendations - to make sure you have everything you need to get set up properly for best results in your online lessons. - If you have everything you need, then you can follow the instructions on our setup help articles to download and install our free app and set it up with your audio gear and webcam. Please email us at support@jamkazam.com or call us at 1-877-376-8742 any time so that we can help you with these steps. We are very happy to help, and we also strongly suggest that you let one of our staff get into an online session with you to make sure everything is working properly and to make sure you're comfortable with the app and ready for your first lesson. -

+

2. Set Up Your Gear
+ Please review our + help + articles on gear recommendations + to make sure you have everything you need to get set up properly for best results in your online lessons. + If you have everything you need, then you can follow the instructions on our + setup + help articles to download and install our free app and set it up with your audio gear and webcam. Please email + us at support@jamkazam.com or call us at + 1-877-376-8742 any time so that we can help you with these steps. + We are very happy to help, and we also strongly suggest that you let one of our staff get into an online session + with you to make sure everything is working properly and to make sure you're comfortable with the app and ready for + your first lesson. +

-

3. Learn About JamClass Features
- Please review our JamClass user guide for students to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to search for the best teacher for you, how to request/book lessons, how to join your teacher in online lessons, features you can use while in lessons, and much more. -

+

3. Learn About JamClass Features
+ Please review our + JamClass + user guide for students to familiarize yourself with the features and resources available to you through our + JamClass lesson service. This includes how to search for the best teacher for you, how to request/book lessons, how + to join your teacher in online lessons, features you can use while in lessons, and much more. +

-

+

- Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician! -

+ Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn + and grow as a musician! +

+<% end %>

Best Regards,
Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb index 0590bfa1c..19b30626d 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb @@ -6,56 +6,113 @@

<% end %> -

- Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology was - built from the ground up for playing music live in sync with high quality audio from different locations over the - Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like - Skype, etc. -

+<% if @education %> -

- To get ready to teach JamClass students online, here are the things you'll want to do: -

+

+ Thank you for expressing an interest in teaching private music lessons online using JamKazam! A member of our staff + will reach out to you shortly to chat with you and answer any questions you have about how our online music lesson + service works, to help you determine if this is a good option for you. +

+ +

+ If you decide teaching online lessons look good, then we'll help you get your gear set up properly, and we'll even + get into a sample online session with you to make sure everything is working properly, and to ensure you are + comfortable using our app's features in a lesson. +

+ +

To take online lessons on JamKazam, you'll need the following things at home:

+ + + +

+ For higher quality audio in online sessions, we recommend (this is an option, not a requirement) a premium audio + gear bundle that includes an audio interface (a little box you connect to your computer with a USB cable), a + microphone, a mic cable, and a mic stand. We offer this package for just $49.99, which is less than our cost for + these products, but it makes a big difference in audio quality, so we think it's worth the upgrade. Also, if you + already own an audio interface, it's highly likely you can use what you already have with our free app. +

+ +

+ If you are curious to learn more about how everything works, you can also review our help guide for teachers. Thanks + again for connecting with us, and we look forward to speaking with you soon! +

+ +<% else %> +

+ Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology + was + built from the ground up for playing music live in sync with high quality audio from different locations over the + Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like + Skype, etc. +

+ +

+ To get ready to teach JamClass students online, here are the things you'll want to do: +

-

1. Set Up Your Teacher Profile
- As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find - teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher - profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being - found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want to - take a little time to fill in the information in your teacher profile to present yourself well. - Click - here for - instructions on filling out your teacher profile. -

+

1. Set Up Your Teacher Profile
+ As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find + teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher + profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being + found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want + to + take a little time to fill in the information in your teacher profile to present yourself well. + Click + here for + instructions on filling out your teacher profile. +

-

2. Set Up Your Gear
- Click - here for information on the gear requirements to effectively teach using the JamClass service. When you have - everything you need, - use - this set of help articles as a good step-by-step guide to set up your gear for use with the - JamKazam application. After you have signed up, someone from JamKazam will contact you to schedule a test online - session, in which we will make sure your audio and video gear are working properly in an online session, and to make - sure you feel comfortable with the key features you will be using in sessions with students. -

+

2. Set Up Your Gear
+ <% if @education %> + Click + here for information on the gear requirements to effectively teach using the JamClass service. At a minimum, + you'll need a Windows or Mac computer and home Internet service, but we also recommend using an audio interface + for superior audio quality. If you already have an audio interface for home recording, you can very likely use the + one you have. If not, JamKazam offers a high quality audio package of an audio interface (a small box you connect + to your computer via USB cable), a microphone, a mic cable, and a mic stand for just $49.99 (less than our cost). + After you have signed up, someone from JamKazam will contact you to schedule a 1:1 help session to help you get + set up, to make sure your audio and video gear are working properly in an online session, and to make sure you + feel comfortable with the key features you will be using in sessions with students. + <% else %> + Click + here for information on the gear requirements to effectively teach using the JamClass service. When you have + everything you need, + use + this set of help articles as a good step-by-step guide to set up your gear for use with the + JamKazam application. After you have signed up, someone from JamKazam will contact you to schedule a test + online + session, in which we will make sure your audio and video gear are working properly in an online session, and to + make + sure you feel comfortable with the key features you will be using in sessions with students. + <% end %> +

+ +

3. Learn About JamClass Features
+ Click + this link for a set of help articles specifically for teachers to learn how to respond to student lesson + requests, how to join your lessons when they are scheduled to begin, how to get paid, and more. You can also + use + this + link for a set of help articles that explain how to use the key features available to you in online sessions + to + effectively teach students. +

+ +

+ As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for + help. You can email us any time at support@jamkazam.com. + We are happy to help you, and we look forward to helping you + reach and teach more students! +

+<% end %> -

3. Learn About JamClass Features
- Click - this link for a set of help articles specifically for teachers to learn how to respond to student lesson - requests, how to join your lessons when they are scheduled to begin, how to get paid, and more. You can also - use - this - link for a set of help articles that explain how to use the key features available to you in online sessions to - effectively teach students. -

-

- As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for - help. You can email us any time at support@jamkazam.com. - We are happy to help you, and we look forward to helping you - reach and teach more students! -

Best Regards,
Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 46c144d9e..d776c2fb9 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -3,6 +3,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base 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_one :retailer, class_name: "JamRuby::Retailer" 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 @@ -94,6 +95,17 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base oo.save end + def self.create_from_retailer(retailer) + oo = AffiliatePartner.new + oo.partner_name = "Affiliate from Retailer #{retailer.id}" + oo.partner_user = retailer.owner + oo.entity_type = 'Other' + oo.retailer = retailer + 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 diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb index 8ae916979..7854d712c 100644 --- a/ruby/lib/jam_ruby/models/charge.rb +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -81,7 +81,7 @@ module JamRuby subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (unhandled)" body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" - AdminMailer.alerts({subject: subject, body: body}).deliver + AdminMailer.alerts({subject: subject, body: body}).deliver_now return false end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index aa389686a..b9af3eaf8 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -13,6 +13,7 @@ module JamRuby belongs_to :jam_track, class_name: "JamRuby::JamTrack" belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right belongs_to :last_stem, class_name: 'JamRuby::JamTrackTrack', foreign_key: 'last_stem_id', inverse_of: :jam_track_right + belongs_to :posa_card, class_name: 'JamRuby::PosaCard' #unused validates :version, presence: true validates :user, presence: true diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 505257dc3..8a2688f06 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -46,6 +46,7 @@ module JamRuby belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy belongs_to :school, class_name: "JamRuby::School" belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice" + belongs_to :posa_card, class_name: "JamRuby::PosaCard" has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy has_many :lesson_sessions, class_name: "JamRuby::LessonSession", :dependent => :destroy has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase", :dependent => :destroy @@ -79,14 +80,14 @@ module JamRuby after_create :after_create around_save :around_update - scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) } - scope :active, -> { where(active: true) } - scope :approved, -> { where(status: STATUS_APPROVED) } - scope :requested, -> { where(status: STATUS_REQUESTED) } - scope :canceled, -> { where(status: STATUS_CANCELED) } - scope :suspended, -> { where(status: STATUS_SUSPENDED) } - scope :engaged, -> { where(ENGAGED) } - scope :engaged_or_successful, -> { where("(" + ENGAGED + ") OR (lesson_bookings.status = '#{STATUS_COMPLETED}' AND lesson_bookings.success = true)")} + scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) } + scope :active, -> { where(active: true) } + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :engaged, -> { where(ENGAGED) } + scope :engaged_or_successful, -> { where("(" + ENGAGED + ") OR (lesson_bookings.status = '#{STATUS_COMPLETED}' AND lesson_bookings.success = true)") } def before_validation if self.booked_price.nil? @@ -95,7 +96,7 @@ module JamRuby end def after_create - if (card_presumed_ok || school_on_school?) && !sent_notices + if (posa_card || card_presumed_ok || !payment_if_school_on_school?) && !sent_notices send_notices end end @@ -128,13 +129,14 @@ module JamRuby else if current_lesson.nil? puts "OHOHOMOOMG #{self.inspect}" - raise "no purchase assigned to lesson booking for lesson!" + raise "no purchase assigned to lesson booking for lesson!" end real_price = self.current_lesson.teacher_distribution.jamkazam_margin end {price: real_price, real_price: real_price, total_price: real_price} end + # here for shopping_cart def price booked_price @@ -215,13 +217,18 @@ module JamRuby def sync_remaining_test_drives if is_test_drive? || is_single_free? - if card_presumed_ok && !user_decremented + if (posa_card || card_presumed_ok) && !user_decremented self.user_decremented = true self.save(validate: false) if is_single_free? user.remaining_free_lessons = user.remaining_free_lessons - 1 elsif is_test_drive? - user.remaining_test_drives = user.remaining_test_drives - 1 + if posa_card + user.jamclass_credits = user.jamclass_credits - 1 + else + user.remaining_test_drives = user.remaining_test_drives - 1 + end + end user.save(validate: false) end @@ -313,7 +320,7 @@ module JamRuby times << time end end - { times: times, session: sessions.first } + {times: times, session: sessions.first} end def determine_needed_sessions(sessions) @@ -393,8 +400,8 @@ module JamRuby end def requires_teacher_distribution?(target) - if school_on_school? - false + if no_school_on_school_payment? + return false elsif target.is_a?(JamRuby::LessonSession) is_test_drive? || (is_normal? && !is_monthly_payment?) elsif target.is_a?(JamRuby::LessonPackagePurchase) @@ -520,7 +527,17 @@ module JamRuby end end - def distribution_price_in_cents(target) + def distribution_price_in_cents(target, education) + distribution = teacher_distribution_price_in_cents(target) + + if education + (distribution * 0.0625).round + else + distribution + end + end + + def teacher_distribution_price_in_cents(target) if is_single_free? 0 elsif is_test_drive? @@ -557,14 +574,21 @@ module JamRuby def dayWeekDesc(slot = default_slot) day = case slot.day_of_week - when 0 then "Sunday" - when 1 then "Monday" - when 2 then "Tuesday" - when 3 then "Wednesday" - when 4 then "Thursday" - when 5 then "Friday" - when 6 then "Saturday" - end + when 0 then + "Sunday" + when 1 then + "Monday" + when 2 then + "Tuesday" + when 3 then + "Wednesday" + when 4 then + "Thursday" + when 5 then + "Friday" + when 6 then + "Saturday" + end if slot.hour > 11 @@ -596,6 +620,7 @@ module JamRuby save self end + def cancel(canceler, other, message) self.canceling = true @@ -613,12 +638,12 @@ module JamRuby end end if approved_before? - # just tell both people it's cancelled, to act as confirmation - Notification.send_lesson_message('canceled', next_lesson, false) - Notification.send_lesson_message('canceled', next_lesson, true) - UserMailer.student_lesson_booking_canceled(self, message).deliver_now - UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now - purpose = "Lesson Canceled" + # just tell both people it's cancelled, to act as confirmation + Notification.send_lesson_message('canceled', next_lesson, false) + Notification.send_lesson_message('canceled', next_lesson, true) + UserMailer.student_lesson_booking_canceled(self, message).deliver_now + UserMailer.teacher_lesson_booking_canceled(self, message).deliver_now + purpose = "Lesson Canceled" else if canceler == student # if it's the first time acceptance student canceling, we call it a 'cancel' @@ -659,10 +684,16 @@ module JamRuby # errors.add(:user, 'has no credit card stored') #end elsif is_test_drive? - if !user.has_test_drives? && !user.can_buy_test_drive? - errors.add(:user, "have no remaining test drives") - elsif teacher.has_booked_test_drive_with_student?(user) && !user.admin - errors.add(:user, "have an in-progress or successful TestDrive with this teacher already") + if posa_card + if !user.has_posa_credits? + errors.add(:user, "have no remaining jamclass credits") + end + else + if !user.has_test_drives? && !user.can_buy_test_drive? + errors.add(:user, "have no remaining test drives") + elsif teacher.has_booked_test_drive_with_student?(user) && !user.admin + errors.add(:user, "have an in-progress or successful TestDrive with this teacher already") + end end @@ -724,6 +755,7 @@ module JamRuby def self.book_packaged_test_drive(user, teacher, description, test_drive_package_choice) book_test_drive(user, teacher, LessonBookingSlot.packaged_slots, description, test_drive_package_choice) end + def self.book_free(user, teacher, lesson_booking_slots, description) self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) end @@ -752,15 +784,29 @@ module JamRuby lesson_booking.payment_style = payment_style lesson_booking.description = description lesson_booking.status = STATUS_REQUESTED - lesson_booking.test_drive_package_choice = test_drive_package_choice + if lesson_type == LESSON_TYPE_TEST_DRIVE + # if the user has any jamclass credits, then we should get their most recent posa purchase + if user.jamclass_credits > 0 + lesson_booking.posa_card = user.most_recent_posa_purchase.posa_card + else + # otherwise, it's a normal test drive, and we should honor test_drive_package_choice if specified + lesson_booking.test_drive_package_choice = test_drive_package_choice + end + end + + if lesson_booking.teacher && lesson_booking.teacher.teacher.school lesson_booking.school = lesson_booking.teacher.teacher.school end if user lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id)) + if lesson_booking.same_school + lesson_booking.same_school_free = !user.school.education # non-education schools (music schools) are 'free' when school-on-school + end else lesson_booking.same_school = false + lesson_booking.same_school_free = false end # two-way association slots, for before_validation loic in slot to work @@ -779,7 +825,7 @@ module JamRuby end def self.unprocessed(current_user) - LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where('school_id IS NULL') + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where(same_school_free: false).where(posa_card:nil) end def self.requested(current_user) @@ -790,6 +836,19 @@ module JamRuby same_school end + def school_on_school_payment? + !!(same_school && school.education) + end + + def no_school_on_school_payment? + !!(school_on_school? && !school_on_school_payment?) + end + + # if this is school-on-school, is payment required? + def payment_if_school_on_school? + !!(!school_on_school? || school_on_school_payment?) + end + def school_and_teacher if school && school.scheduling_comm? [school.communication_email, teacher.email] @@ -862,7 +921,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) + .where(same_school_free: false) .active .where('music_sessions.scheduled_start >= ?', current_month_first_day) .where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq @@ -908,6 +967,7 @@ module JamRuby def self.not_failed end + def self.engaged_bookings(student, teacher, since_at = nil) bookings = bookings(student, teacher, since_at) bookings.engaged_or_successful diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index 2f88af5c5..dd3aa914e 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -4,7 +4,9 @@ module JamRuby @@log = Logging.logger[LessonPackagePurchase] - delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :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 + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :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 delegate :test_drive_count, to: :lesson_package_type # who purchased the lesson package? @@ -13,8 +15,9 @@ module JamRuby belongs_to :teacher, class_name: "JamRuby::User" belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + belongs_to :posa_card, class_name: "JamRuby::PosaCard", foreign_key: :posa_card_id has_one :lesson_session, class_name: "JamRuby::LessonSession", dependent: :destroy - has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + has_many :teacher_distributions, class_name: "JamRuby::TeacherDistribution" has_one :sale_line_item, class_name: "JamRuby::SaleLineItem", dependent: :destroy @@ -28,6 +31,10 @@ module JamRuby def validate_test_drive if user + # if this is a posa card purchase, we won't stop it from getting created + if posa_card_id + return + end if lesson_package_type.is_test_drive? && !user.can_buy_test_drive? errors.add(:user, "can not buy test drive right now because you have already purchased it within the last year") end @@ -35,7 +42,7 @@ module JamRuby end def create_charge - if !school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? + if payment_if_school_on_school? && lesson_booking && lesson_booking.is_monthly_payment? self.lesson_payment_charge = LessonPaymentCharge.new lesson_payment_charge.user = user lesson_payment_charge.amount_in_cents = 0 @@ -45,16 +52,27 @@ module JamRuby end end + def teacher_distribution + teacher_distributions.where(education:false).first + end + + def education_distribution + teacher_distributions.where(education:true).first + end + def add_test_drives + if posa_card_id + #user.jamclass_credits incremented in posa_card.rb + return + end + if self.lesson_package_type.is_test_drive? new_test_drives = user.remaining_test_drives + lesson_package_type.test_drive_count User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) user.remaining_test_drives = new_test_drives end - end - def to_s "#{name}" end @@ -67,21 +85,28 @@ module JamRuby lesson_payment_charge.amount_in_cents / 100.0 end - def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil) + def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil, posa_card = nil) purchase = LessonPackagePurchase.new purchase.user = user purchase.lesson_booking = lesson_booking purchase.teacher = lesson_booking.teacher if lesson_booking + purchase.posa_card = posa_card if year purchase.year = year purchase.month = month purchase.recurring = true + # this is for monthly if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) - purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false) + purchase.teacher_distributions << teacher_dist # price should always match the teacher_distribution, if there is one - purchase.price = purchase.teacher_distribution.amount_in_cents / 100 + purchase.price = teacher_dist.amount_in_cents / 100 + + if lesson_booking.school_on_school_payment? + purchase.teacher_distributions << TeacherDistribution.create_for_lesson_package_purchase(purchase, true) + end end else purchase.recurring = false @@ -136,10 +161,23 @@ module JamRuby end end + def school_on_school_payment? + !!(school_on_school? && teacher.teacher.school.education) + end + + def no_school_on_school_payment? + !!(school_on_school? && !school_on_school_payment?) + end + + # if this is school-on-school, is payment required? + def payment_if_school_on_school? + !!(!school_on_school? || school_on_school_payment?) + end def bill_monthly(force = false) - if school_on_school? + if !payment_if_school_on_school? + puts "SCHOOL ON SCHOOL PAYMENT OH NO" raise "school-on-school: should not be here" else lesson_payment_charge.charge(force) diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb index c4f5dd508..b4bb267a3 100644 --- a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -63,11 +63,8 @@ module JamRuby post_sale_test_failure - distribution = target.teacher_distribution - if distribution # not all lessons/payment charges have a distribution - distribution.ready = true - distribution.save(validate: false) - end + target.teacher_distributions.update_all(ready:true) # possibly there are 0 distributions on this lesson + stripe_charge end @@ -103,7 +100,9 @@ module JamRuby end def expected_price_in_cents - target.lesson_booking.distribution_price_in_cents(target) + distribution = target.teacher_distribution + for_education = distribution && distribution.education + target.lesson_booking.distribution_price_in_cents(target, for_education) end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index c9de915a8..f0b3bbb2f 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -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?, :school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, to: :lesson_booking + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :school_on_school_payment?, :no_school_on_school_payment?, :payment_if_school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, :posa_card, to: :lesson_booking delegate :pretty_scheduled_start, to: :music_session @@ -41,7 +41,7 @@ module JamRuby belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id, :dependent => :destroy belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson, :dependent => :destroy - has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution", dependent: :destroy + has_many :teacher_distributions, class_name: "JamRuby::TeacherDistribution", dependent: :destroy has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id" has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" @@ -86,7 +86,7 @@ module JamRuby .order('music_sessions.scheduled_start DESC') } def create_charge - if !school_on_school? && !is_test_drive? && !is_monthly_payment? + if payment_if_school_on_school? && !is_test_drive? && !is_monthly_payment? self.lesson_payment_charge = LessonPaymentCharge.new lesson_payment_charge.user = @assigned_student lesson_payment_charge.amount_in_cents = 0 @@ -96,6 +96,14 @@ module JamRuby end end + def teacher_distribution + teacher_distributions.where(education:false).first + end + + def education_distribution + teacher_distributions.where(education:true).first + end + 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. @@ -209,7 +217,10 @@ module JamRuby self.status = STATUS_COMPLETED if success && lesson_booking.requires_teacher_distribution?(self) - self.teacher_distribution = TeacherDistribution.create_for_lesson(self) + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) + if lesson_booking.school_on_school_payment? + self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true) + end end if self.save @@ -292,7 +303,7 @@ module JamRuby end def bill_lesson - if school_on_school? + if no_school_on_school_payment? success = true else lesson_payment_charge.charge @@ -341,14 +352,9 @@ module JamRuby def test_drive_completed - distribution = teacher_distribution - if !sent_notices if success - if distribution # not all lessons/payment charges have a distribution - distribution.ready = true - distribution.save(validate: false) - end + teacher_distributions.update_all(ready:true) # possibly there are 0 distributions on this lesson student.test_drive_succeeded(self) else student.test_drive_failed(self) @@ -387,7 +393,7 @@ module JamRuby else if lesson_booking.is_monthly_payment? if !sent_notices - if !school_on_school? + if payment_if_school_on_school? # bad session; just poke user UserMailer.monthly_recurring_no_bill(self).deliver_now end @@ -401,7 +407,7 @@ module JamRuby else if !sent_notices - if !school_on_school? + if payment_if_school_on_school? # bad session; just poke user UserMailer.student_lesson_normal_no_bill(self).deliver_now end @@ -422,7 +428,7 @@ module JamRuby bill_lesson else if !sent_notices - if !school_on_school? + if payment_if_school_on_school? UserMailer.student_lesson_normal_no_bill(self).deliver_now UserMailer.teacher_lesson_normal_no_bill(self).deliver_now end @@ -575,8 +581,12 @@ module JamRuby lesson_session.slot = booking.default_slot lesson_session.assigned_student = booking.student lesson_session.user = booking.student - if booking.is_test_drive? && booking.student.remaining_test_drives > 0 - lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase + if booking.is_test_drive? + if booking.student.jamclass_credits > 0 + lesson_session.lesson_package_purchase = booking.student.most_recent_posa_purchase + elsif booking.student.remaining_test_drives > 0 + lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase + end end lesson_session.save @@ -659,7 +669,8 @@ 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 = ?) ' + school_extra, user.id) + # only show 'fully booked lessons'; not those they can not possibly be paid for + query = query.where('lesson_bookings.posa_card_id IS NOT NULL OR 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 @@ -733,7 +744,11 @@ module JamRuby # 1st time this has ever been approved; there are other things we need to do if lesson_package_purchase.nil? && lesson_booking.is_test_drive? - self.lesson_package_purchase = student.most_recent_test_drive_purchase + if student.jamclass_credits > 0 + self.lesson_package_purchase = student.most_recent_posa_purchase + elsif student.remaining_test_drives > 0 + self.lesson_package_purchase = student.most_recent_test_drive_purchase + end end if self.save diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 572d137a1..9ced0fe74 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -76,6 +76,7 @@ module JamRuby end def can_download?(some_user) + return false if some_user.nil? claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id) if claimed_recording diff --git a/ruby/lib/jam_ruby/models/posa_card.rb b/ruby/lib/jam_ruby/models/posa_card.rb new file mode 100644 index 000000000..beb1bfbd4 --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card.rb @@ -0,0 +1,182 @@ +# represents the gift card you hold in your hand +module JamRuby + class PosaCard < ActiveRecord::Base + + @@log = Logging.logger[PosaCard] + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_CLASS_4 = 'jam_class_4' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10, + JAM_CLASS_4 + ] + + + belongs_to :user, class_name: "JamRuby::User" + belongs_to :retailer, class_name: "JamRuby::Retailer" + has_many :posa_card_purchases, class_name: 'JamRuby::PosaCardPurchase' + has_one :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + has_one :jam_track_right, class_name: "JamRuby::JamTrackRight" + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + validates :code, presence: true, uniqueness: true + + after_save :check_attributed + + validate :already_activated + validate :retailer_set + validate :already_claimed + validate :user_set + validate :must_be_activated + validate :within_one_year + + def is_lesson_posa_card? + card_type == JAM_CLASS_4 + end + + def credits + if card_type == JAM_TRACKS_5 + 5 + elsif card_type == JAM_TRACKS_10 + 10 + elsif card_type == JAM_CLASS_4 + 4 + else + raise "unknown card type #{card_type}" + end + end + + def already_activated + if activated_at && activated_at_was && activated_at_changed? + if retailer && retailer_id == retailer_id_was + self.errors.add(:activated_at, 'already activated. Please give card to customer. Thank you!') + else + self.errors.add(:activated_at, 'already activated by someone else. Please contact support@jamkaazm.com') + end + end + end + + def within_one_year + if user && claimed_at && claimed_at_was && claimed_at_changed? + if !user.can_claim_posa_card + self.errors.add(:claimed_at, 'was within 1 year') + end + end + end + + def already_claimed + if claimed_at && claimed_at_was && claimed_at_changed? + self.errors.add(:claimed_at, 'already claimed') + end + end + + def retailer_set + if activated_at && !retailer + self.errors.add(:retailer, 'must be specified') + end + end + + def user_set + if claimed_at && !user + self.errors.add(:user, 'must be specified') + end + end + + def must_be_activated + if claimed_at && !activated_at + self.errors.add(:activated_at, 'must already be set') + end + end + + def check_attributed + if user && user_id_changed? + if card_type == JAM_TRACKS_5 + user.gifted_jamtracks += 5 + elsif card_type == JAM_TRACKS_10 + user.gifted_jamtracks += 10 + elsif card_type == JAM_CLASS_4 + user.jamclass_credits += 4 + else + raise "unknown card type #{card_type}" + end + user.save! + end + end + + def lesson_package_type + if card_type == JAM_TRACKS_5 + raise 'not a lesson package: ' + card_type + elsif card_type == JAM_TRACKS_10 + raise 'not a lesson package: ' + card_type + elsif card_type == JAM_CLASS_4 + LessonPackageType.test_drive_4 + else + raise "unknown card type #{card_type}" + end + end + + def product_info + price = nil + plan_code = nil + + if card_type == JAM_TRACKS_5 + price = 9.99 + plan_code = 'posa-jamtracks-5' + elsif card_type == JAM_TRACKS_10 + price = 19.99 + plan_code = 'posa-jatracks-10' + elsif card_type == JAM_CLASS_4 + price = 49.99 + plan_code = 'posa-jamclass-4' + else + raise "unknown card type #{card_type}" + end + {price: price, quantity: 1, marked_for_redeem: false, plan_code: plan_code} + end + + def self.activate(posa_card, retailer) + Sale.posa_activate(posa_card, retailer) + end + + def activate(retailer) + self.activated_at = Time.now + self.retailer = retailer + + self.save + end + + def claim(user) + self.user = user + self.claimed_at = Time.now + + + if self.save + UserWhitelist.card_create(user, 'posa') + SaleLineItem.associate_user_for_posa(self, user) + + # when you claim a POSA card, you are also making a LessonPackagePurchase + if is_lesson_posa_card? + purchase = LessonPackagePurchase.create(user, nil, lesson_package_type, nil, nil, self) if purchase.nil? + end + end + end + + def short_display + if card_type == JAM_TRACKS_5 + 'JT-5' + elsif card_type == JAM_TRACKS_10 + 'JT-10' + elsif card_type == JAM_CLASS_4 + 'JC-4' + else + raise "unknown card type #{card_type}" + end + end + def to_s + "POSA #{short_display} #{code}" + end + end +end diff --git a/ruby/lib/jam_ruby/models/posa_card_purchase.rb b/ruby/lib/jam_ruby/models/posa_card_purchase.rb new file mode 100644 index 000000000..4a5356188 --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card_purchase.rb @@ -0,0 +1,18 @@ +# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard) +module JamRuby + class PosaCardPurchase < ActiveRecord::Base + + @@log = Logging.logger[PosaCardPurchase] + + attr_accessible :user, :posa_card_type + + def name + posa_card_type.sale_display + end + + # who purchased the card? + belongs_to :user, class_name: "JamRuby::User" + belongs_to :posa_card_type, class_name: "JamRuby::PosaCardType" + belongs_to :posa_card, class_name: 'JamRuby::PosaCard' + end +end diff --git a/ruby/lib/jam_ruby/models/posa_card_type.rb b/ruby/lib/jam_ruby/models/posa_card_type.rb new file mode 100644 index 000000000..fb2c70e78 --- /dev/null +++ b/ruby/lib/jam_ruby/models/posa_card_type.rb @@ -0,0 +1,84 @@ +# reperesents the posa card you buy from the site +module JamRuby + class PosaCardType < ActiveRecord::Base + + @@log = Logging.logger[PosaCardType] + + PRODUCT_TYPE = 'PosaCardType' + + + JAM_TRACKS_5 = 'jam_tracks_5' + JAM_TRACKS_10 = 'jam_tracks_10' + JAM_CLASS_4 = 'jam_class_4' + CARD_TYPES = + [ + JAM_TRACKS_5, + JAM_TRACKS_10, + JAM_CLASS_4 + ] + + validates :card_type, presence: true, inclusion: {in: CARD_TYPES} + + def self.jam_track_5 + PosaCardType.find(JAM_TRACKS_5) + end + + def self.jam_track_10 + PosaCardType.find(JAM_TRACKS_10) + end + + def self.jam_class_4 + PosaCardType.find(JAM_CLASS_4) + end + + + def name + sale_display + end + + def price + if card_type == JAM_TRACKS_5 + 10.00 + elsif card_type == JAM_TRACKS_10 + 20.00 + elsif card_type == JAM_CLASS_4 + 49.99 + else + raise "unknown card type #{card_type}" + end + end + + + def sale_display + if card_type == JAM_TRACKS_5 + 'JamTracks Card (5)' + elsif card_type == JAM_TRACKS_10 + 'JamTracks Card (10)' + elsif card_type == JAM_TRACKS_10 + 'JamClass Card (4)' + else + raise "unknown card type #{card_type}" + end + end + + def plan_code + if card_type == JAM_TRACKS_5 + "jamtrack-posacard-5" + elsif card_type == JAM_TRACKS_10 + "jamtrack-posacard-10" + elsif card_type == JAM_CLASS_4 + "jamclass-posacard-4" + else + raise "unknown card type #{card_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/retailer.rb b/ruby/lib/jam_ruby/models/retailer.rb new file mode 100644 index 000000000..50185e5eb --- /dev/null +++ b/ruby/lib/jam_ruby/models/retailer.rb @@ -0,0 +1,147 @@ +module JamRuby + class Retailer < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:name] + + attr_accessor :updating_avatar, :password, :should_validate_password + 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 :posa_cards, class_name: 'JamRuby::PosaCard' + belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_retailer + belongs_to :affiliate_partner, class_name: "JamRuby::AffiliatePartner" + has_many :teachers, class_name: "JamRuby::Teacher" + has_many :retailer_invitations, class_name: 'JamRuby::RetailerInvitation' + has_many :teacher_payments, class_name: 'JamRuby::TeacherPayment' + has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' + has_many :sales, class_name: 'JamRuby::Sale' + has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' + + validates :user, presence: true + #validates :slug, presence: true + validates :enabled, inclusion: {in: [true, false]} + validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password + + after_create :create_affiliate + after_create :create_slug + # before_save :stringify_avatar_info, :if => :updating_avatar + + def create_slug + if self.slug.blank? + puts "SELF ID #{self.id}" + self.slug = self.id.to_s + end + + self.save! + end + + def create_affiliate + AffiliatePartner.create_from_retailer(self) + end + + def encrypt(password) + BCrypt::Password.create(password, cost: 12).to_s + end + + def matches_password(password) + + if password.blank? + return false + end + + puts "self.encrypted_password #{self.encrypted_password}" + begin + # we init passwordfield as a UUID, which is a bogus hash; so if we see UUId, we know retailer has no password yet + UUIDTools::UUID.parse(self.encrypted_password) + return false + rescue ArgumentError + end + + BCrypt::Password.new(self.encrypted_password) == password + end + + def update_from_params(params) + self.name = params[:name] if params[:name].present? + self.city = params[:city] + self.state = params[:state] + self.slug = params[:slug] if params[:slug].present? + + if params[:password].present? + self.should_validate_password = true + self.password = params[:password] + self.encrypted_password = encrypt(params[:password]) + end + self.save + end + + def owner + user + end + + def validate_avatar_info + if updating_avatar + # we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io) + errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil? + errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil? + errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil? + end + end + + def escape_filename(path) + dir = File.dirname(path) + file = File.basename(path) + "#{dir}/#{ERB::Util.url_encode(file)}" + end + + def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket) + self.updating_avatar = true + + cropped_s3_path = cropped_fpfile["key"] + cropped_large_s3_path = cropped_large_fpfile["key"] + + self.update_attributes( + :original_fpfile => original_fpfile.to_json, + :cropped_fpfile => cropped_fpfile.to_json, + :cropped_large_fpfile => cropped_large_fpfile.to_json, + :cropped_s3_path => cropped_s3_path, + :cropped_large_s3_path => cropped_large_s3_path, + :crop_selection => crop_selection.to_json, + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) + end + + def delete_avatar(aws_bucket) + + User.transaction do + + unless self.cropped_s3_path.nil? + S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg') + S3Util.delete(aws_bucket, self.cropped_s3_path) + S3Util.delete(aws_bucket, self.cropped_large_s3_path) + end + + return self.update_attributes( + :original_fpfile => nil, + :cropped_fpfile => nil, + :cropped_large_fpfile => nil, + :cropped_s3_path => nil, + :cropped_large_s3_path => nil, + :photo_url => nil, + :crop_selection => nil, + :large_photo_url => nil + ) + end + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? && original_fpfile.class != String + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? && cropped_fpfile.class != String + self.crop_selection = crop_selection.to_json if !crop_selection.nil? && crop_selection.class != String + end + end +end diff --git a/ruby/lib/jam_ruby/models/retailer_invitation.rb b/ruby/lib/jam_ruby/models/retailer_invitation.rb new file mode 100644 index 000000000..dad1f18f2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/retailer_invitation.rb @@ -0,0 +1,89 @@ +module JamRuby + class RetailerInvitation < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:note] + + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :retailer, class_name: ::JamRuby::Retailer + + validates :retailer, presence: true + validates :email, email: true + validates :invitation_code, presence: true + validates :accepted, inclusion: {in: [true, false]} + validates :first_name, presence: true + validates :last_name, presence: true + validate :retailer_has_name, on: :create + + before_validation(on: :create) do + self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? + end + + def retailer_has_name + if retailer && retailer.name.blank? + errors.add(:retailer, "must have name") + end + end + + def self.index(retailer, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = RetailerInvitation.where(retailer_id: retailer.id) + query = query.includes([:user, :retailer]) + query = query.order('created_at') + query = query.where(accepted:false) + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + + + def self.create(current_user, specified_retailer, params) + + invitation = RetailerInvitation.new + invitation.retailer = specified_retailer + invitation.email = params[:email] + invitation.first_name = params[:first_name] + invitation.last_name = params[:last_name] + + if invitation.save + invitation.send_invitation + end + invitation + end + + + def send_invitation + UserMailer.invite_retailer_teacher(self).deliver_now + end + def generate_signup_url + "#{APP_CONFIG.external_root_url}/retailer/#{retailer.id}/teacher?invitation_code=#{self.invitation_code}" + end + + def delete + self.destroy + end + + def resend + send_invitation + end + + + + end +end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 37dc846af..5f6ff9c05 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -5,17 +5,20 @@ module JamRuby JAMTRACK_SALE = 'jamtrack' LESSON_SALE = 'lesson' + POSA_SALE = 'posacard' SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' + belongs_to :retailer, class_name: 'JamRuby::Retailer' belongs_to :user, class_name: 'JamRuby::User' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id' validates :order_total, numericality: {only_integer: false} - validates :user, presence: true + #validates :user + #validates :retailer @@log = Logging.logger[Sale] @@ -215,6 +218,26 @@ module JamRuby def self.post_sale_test_failure return true end + + def self.posa_activate(posa_card, retailer) + sale = nil + Sale.transaction(:requires_new => true) do + + posa_card.activate(retailer) + + if !posa_card.errors.any? + + sale = create_posa_sale(retailer, posa_card) + + SaleLineItem.create_from_posa_card(sale, retailer, posa_card) + + sale.save + end + + end + {sale: sale} + end + # this is easy to make generic, but right now, it just purchases lessons def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false) stripe_charge = nil @@ -223,43 +246,43 @@ module JamRuby # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it Sale.transaction(:requires_new => true) do - sale = create_lesson_sale(current_user) + sale = create_lesson_sale(current_user) - if sale.valid? + 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(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) - - post_sale_test_failure - - if price_info[:purchase] && price_info[:purchase].errors.any? - purchase = price_info[:purchase] - raise ActiveRecord::Rollback - end - - if !sale_line_item.valid? - raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" - end - # sale.source = 'stripe' - sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] - sale.recurly_tax_in_cents = price_info[:tax_in_cents] - sale.recurly_total_in_cents = price_info[:total_in_cents] - sale.recurly_currency = price_info[:currency] - sale.stripe_charge_id = price_info[:charge_id] - sale.save - stripe_charge = price_info[:charge] - 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" + 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(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) + + post_sale_test_failure + + if price_info[:purchase] && price_info[:purchase].errors.any? + purchase = price_info[:purchase] + raise ActiveRecord::Rollback + end + + if !sale_line_item.valid? + raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" + end + # sale.source = 'stripe' + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.stripe_charge_id = price_info[:charge_id] + sale.save + stripe_charge = price_info[:charge] + 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} @@ -304,20 +327,34 @@ module JamRuby tax_in_cents = (subtotal_in_cents * tax_percent).round total_in_cents = subtotal_in_cents + tax_in_cents - lesson_id = lesson_session.id if lesson_session # not set if test drive + if lesson_session # not set if test drive + lesson_id = lesson_session.id + teacher_id = lesson_session.teacher.id + teacher_name = lesson_session.teacher.name + end + charge_id = charge.id if charge # not set if test drive + begin + metadata = { + lesson_package: purchase.id, + lesson_session: lesson_id, + teacher_id: teacher_id, + teacher_name: teacher_name, + charge: charge_id, + user: current_user.id, + tax: tax_in_cents + } + rescue Exception => e + metadata = {metaerror: true} + end + stripe_charge = Stripe::Charge.create( :amount => total_in_cents, :currency => "usd", :customer => current_user.stripe_customer_id, :description => target.stripe_description(lesson_booking), - :metadata => { - lesson_package: purchase.id, - lesson_session: lesson_id, - charge: charge_id, - user: current_user.id - } + :metadata => metadata ) if charge charge.stripe_charge = stripe_charge @@ -434,6 +471,7 @@ module JamRuby end unless sale.save + puts "WTF" raise RecurlyClientError, "Invalid sale (at end)." end rescue Recurly::Resource::Invalid => e @@ -441,6 +479,8 @@ module JamRuby sale.rollback_adjustments(current_user, created_adjustments) sale = nil raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic + rescue => e + puts "UNKNOWN E #{e}" end else raise RecurlyClientError, "Could not find account to place order." @@ -634,6 +674,15 @@ module JamRuby sale end + def self.create_posa_sale(retailer, posa_card) + sale = Sale.new + sale.retailer = retailer + sale.sale_type = POSA_SALE # gift cards and jam tracks are sold with this type of sale + sale.order_total = posa_card.product_info[:price] + sale.save + sale + end + # this checks just jamtrack sales appropriately def self.check_integrity_of_jam_track_sales Sale.select([:total, :voided]).find_by_sql( diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 7dec6502d..eb053e144 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -6,12 +6,14 @@ module JamRuby JAMTRACK = 'JamTrack' GIFTCARD = 'GiftCardType' LESSON = 'LessonPackageType' + POSACARD = 'PosaCard' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' belongs_to :gift_card, class_name: 'JamRuby::GiftCard' belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + belongs_to :retailer, class_name: 'JamRuby::Retailer' # deprecated; use affiliate_distribution !! belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id @@ -20,7 +22,7 @@ module JamRuby has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' - validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON, POSACARD]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -45,8 +47,11 @@ module JamRuby GiftCardType.find_by_id(product_id) elsif product_type == LESSON lesson_package_purchase + elsif product_type == POSACARD + PosaCard.find(product_id) else + raise 'unsupported product type' end end @@ -128,6 +133,41 @@ module JamRuby line_item end + def self.associate_user_for_posa(posa_card, user) + sale_line_item = SaleLineItem.where(product_type: POSACARD).where(product_id: posa_card.id).first + if sale_line_item + sale_line_item.sale.user = user + sale_line_item.sale.save! + end + end + + def self.create_from_posa_card(sale, retailer, posa_card) + product_info = posa_card.product_info + sale_line_item = SaleLineItem.new + sale_line_item.retailer = retailer + sale_line_item.product_type = POSACARD + sale_line_item.product_id = posa_card.id + sale_line_item.unit_price = product_info[:price] + sale_line_item.quantity = product_info[:quantity] + sale_line_item.free = product_info[:marked_for_redeem] + sale_line_item.sales_tax = nil + sale_line_item.shipping_handling = 0 + sale_line_item.recurly_plan_code = product_info[:plan_code] + + + #referral_info = retailer.referral_info + + #if referral_info + # sale_line_item.affiliate_distributions << AffiliateDistribution.create(retailer.affiliate_partner, referral_info[:fee_in_cents], sale_line_item) + # sale_line_item.affiliate_referral = retailer.affiliate_partner + # sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] + #end + + sale.sale_line_items << sale_line_item + sale_line_item.save + sale_line_item + end + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil) product_info = shopping_cart.product_info(instance) diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb index 1e7523b50..804407ce6 100644 --- a/ruby/lib/jam_ruby/models/school.rb +++ b/ruby/lib/jam_ruby/models/school.rb @@ -24,12 +24,17 @@ module JamRuby validates :user, presence: true validates :enabled, inclusion: {in: [true, false]} + validates :education, inclusion: {in: [true, false]} validates :scheduling_communication, inclusion: {in: SCHEDULING_COMMS} validates :correspondence_email, email: true, allow_blank: true validate :validate_avatar_info after_create :create_affiliate - before_save :stringify_avatar_info, :if => :updating_avatar + #before_save :stringify_avatar_info, :if => :updating_avatar + + def is_education? + education + end def scheduling_comm? scheduling_communication == SCHEDULING_COMM_SCHOOL @@ -39,6 +44,10 @@ module JamRuby correspondence_email.blank? ? owner.email : correspondence_email end + def approved_teachers + teachers.where('teachers.ready_for_session_at is not null') + end + def create_affiliate AffiliatePartner.create_from_school(self) end @@ -75,12 +84,12 @@ module JamRuby cropped_large_s3_path = cropped_large_fpfile["key"] self.update_attributes( - :original_fpfile => original_fpfile, - :cropped_fpfile => cropped_fpfile, - :cropped_large_fpfile => cropped_large_fpfile, + :original_fpfile => original_fpfile.to_json, + :cropped_fpfile => cropped_fpfile.to_json, + :cropped_large_fpfile => cropped_large_fpfile.to_json, :cropped_s3_path => cropped_s3_path, :cropped_large_s3_path => cropped_large_s3_path, - :crop_selection => crop_selection, + :crop_selection => crop_selection.to_json, :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) ) @@ -119,4 +128,8 @@ module JamRuby self.crop_selection = crop_selection.to_json if !crop_selection.nil? end end + + def teacher_list_url + "#{APP_CONFIG.external_root_url}/school/#{id}/teachers" + end end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index ec1cc7d38..53cab0b24 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -22,6 +22,7 @@ module JamRuby has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers + belongs_to :retailer, :class_name => "JamRuby::Retailer", inverse_of: :teachers validates :user, :presence => true validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction @@ -218,7 +219,18 @@ module JamRuby teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive) teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) teacher.test_drives_per_week = 10 if !params.key?(:test_drives_per_week) # default to 10 in absence of others - teacher.school_id = params[:school_id] if params.key?(:school_id) + if params.key?(:school_id) + teacher.school_id = params[:school_id] + if !teacher.joined_school_at + teacher.joined_school_at = Time.now + end + end + if params.key?(:retailer_id) + teacher.retailer_id = params[:retailer_id] + if !teacher.joined_retailer_at + teacher.joined_retailer_at = Time.now + end + end # How to validate: teacher.validate_introduction = !!params[:validate_introduction] @@ -415,21 +427,7 @@ module JamRuby ## !!!! this is only valid for tests def stripe_account_id=(new_acct_id) - existing = user.stripe_auth - existing.destroy if existing - - user_auth_hash = { - :provider => 'stripe_connect', - :uid => new_acct_id, - :token => 'bogus', - :refresh_token => 'refresh_bogus', - :token_expiration => Date.new(2050, 1, 1), - :secret => "secret" - } - - authorization = user.user_authorizations.build(user_auth_hash) - authorization.save! - + user.stripe_account_id = new_acct_id end # how complete is their profile? @@ -464,5 +462,15 @@ module JamRuby @part_complete[:pct] = complete.round @part_complete end + + def teaches + if instruments.length == 0 + return '' + elsif instruments.length == 2 + return 'Teaches ' + instruments[0].description + ' and ' + instruments[1].description + else + return 'Teaches ' + instruments.map {|i| i.description}.join(', ') + end + end end end diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 0b82910b7..7833cf0f6 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -6,6 +6,7 @@ module JamRuby belongs_to :lesson_session, class_name: "JamRuby::LessonSession" belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" belongs_to :school, class_name: "JamRuby::School" + belongs_to :retailer, class_name: "JamRuby::Retailer" validates :teacher, presence: true validates :amount_in_cents, presence: true @@ -42,24 +43,26 @@ module JamRuby end end - def self.create_for_lesson(lesson_session) - distribution = create(lesson_session) + def self.create_for_lesson(lesson_session, for_education) + distribution = create(lesson_session, for_education) distribution.lesson_session = lesson_session + distribution.education = for_education distribution end - def self.create_for_lesson_package_purchase(lesson_package_purchase) - distribution = create(lesson_package_purchase) + def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education) + distribution = create(lesson_package_purchase, for_education) distribution.lesson_package_purchase = lesson_package_purchase + distribution.education = for_education distribution end - def self.create(target) + def self.create(target, education) distribution = TeacherDistribution.new distribution.teacher = target.teacher distribution.ready = false distribution.distributed = false - distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target) + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education) distribution.school = target.lesson_booking.school distribution end @@ -81,6 +84,7 @@ module JamRuby end def real_distribution + (real_distribution_in_cents / 100.0) end @@ -108,17 +112,21 @@ module JamRuby end def calculate_teacher_fee - if is_test_drive? + if education 0 else - if school - # if school exists, use it's rate - rate = school.jamkazam_rate + if is_test_drive? + 0 else - # otherwise use the teacher's rate - rate = teacher.teacher.jamkazam_rate + 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 # 0.03 is stripe fee that we include in cost of JK fee end - (amount_in_cents * (rate + 0.03)).round end end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 94f2df67c..785ee77a5 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -5,6 +5,7 @@ module JamRuby belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" belongs_to :school, class_name: "JamRuby::School" + belongs_to :retailer, class_name: "JamRuby::Retailer" def self.hourly_check @@ -14,7 +15,11 @@ module JamRuby # pay the school if the payment owns the school; otherwise default to the teacher def payable_teacher if school - school.owner + if school.education + teacher + else + school.owner + end else teacher end @@ -88,17 +93,19 @@ module JamRuby payment.amount_in_cents = payment.teacher_distribution.amount_in_cents payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee + effective_in_cents = payment.amount_in_cents - payment.fee_in_cents + if payment.teacher_payment_charge.nil? charge = TeacherPaymentCharge.new charge.user = payment.payable_teacher - charge.amount_in_cents = (payment.amount_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round + charge.amount_in_cents = (effective_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round charge.fee_in_cents = payment.fee_in_cents charge.teacher_payment = payment payment.teacher_payment_charge = charge # charge.save! else charge = payment.teacher_payment_charge - charge.amount_in_cents = (payment.amount_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round + charge.amount_in_cents = (effective_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round charge.fee_in_cents = payment.fee_in_cents charge.save! end diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb index 3d74167b7..4dff407ce 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -19,17 +19,29 @@ module JamRuby teacher end + def actual_charge_in_cents + amount_in_cents + end def do_charge(force) - # source will let you supply a token. But... how to get a token in this case? + metadata = {} + + begin + metadata = { + teacher_id: teacher.id, + teacher_name: teacher.name, + tax: 0, + } + rescue Exception => e + metadata = {metaerror: true} + end @stripe_charge = Stripe::Charge.create( - :amount => amount_in_cents, + :amount => actual_charge_in_cents, :currency => "usd", :customer => APP_CONFIG.stripe[:source_customer], :description => construct_description, - :destination => teacher.teacher.stripe_account_id, - :application_fee => fee_in_cents, + :metadata => metadata ) stripe_charge diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 7d67b938e..894275058 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -204,11 +204,13 @@ module JamRuby has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user + has_one :owned_retailer, :class_name => "JamRuby::Retailer", inverse_of: :user has_many :test_drive_package_choices, :class_name =>"JamRuby::TestDrivePackageChoice" has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users has_many :proposed_slots, class_name: 'JamRuby::LessonBookingSlot', inverse_of: :proposer, dependent: :destroy, foreign_key: :proposer_id has_many :charges, class_name: 'JamRuby::Charge', dependent: :destroy + has_many :posa_cards, class_name: 'JamRuby::PosaCard', dependent: :destroy before_save :default_anonymous_names before_save :create_remember_token, :if => :should_validate_password? @@ -264,13 +266,27 @@ module JamRuby def after_save if school_interest && !school_interest_was - AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/schools page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/school_interests", subject: "#{email} is interested in schools"}).deliver_now + if education_interest + AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/education page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/education_interests", subject: "#{email} is interested in education"}).deliver_now + else + AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/schools page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/school_interests", subject: "#{email} is interested in schools"}).deliver_now + end if owned_school.nil? school = School.new school.user = self + school.education = education_interest school.save! end end + + if retailer_interest && !retailer_interest_was + AdminMailer.partner({body: "#{email} signed up via the https://www.jamkazam.com/landing/jamclass/retailers page.\n\nFull list is here: https://www.jamkazam.com/admin/admin/retailer_interests", subject: "#{email} is interested in retailer program"}).deliver_now + if owned_retailer.nil? + retailer = Retailer.new + retailer.user = self + retailer.save! + end + end end def update_teacher_pct if teacher @@ -1129,13 +1145,18 @@ module JamRuby teacher = options[:teacher] school_invitation_code = options[:school_invitation_code] school_id = options[:school_id] + retailer_invitation_code = options[:retailer_invitation_code] + retailer_id = options[:retailer_id] + retailer_interest = options[:retailer_interest] school_interest = options[:school_interest] + education_interest = options[:education_interest] origin = options[:origin] test_drive_package_details = options[:test_drive_package] test_drive_package = TestDrivePackage.find_by_name(test_drive_package_details[:name]) if test_drive_package_details school = School.find(school_id) if school_id + retailer = School.find(retailer_id) if retailer_id user = User.new user.validate_instruments = true UserManager.active_record_transaction do |user_manager| @@ -1150,6 +1171,16 @@ module JamRuby end end + if retailer_invitation_code + retailer_invitation = RetailerInvitation.find_by_invitation_code(retailer_invitation_code) + if retailer_invitation + first_name ||= retailer_invitation.first_name + last_name ||= retailer_invitation.last_name + retailer_invitation.accepted = true + retailer_invitation.save + end + end + user.first_name = first_name if first_name.present? user.last_name = last_name if last_name.present? user.email = email @@ -1157,10 +1188,13 @@ module JamRuby user.terms_of_service = terms_of_service user.reuse_card unless reuse_card.nil? user.gifted_jamtracks = 0 + user.jamclass_credits = 0 user.has_redeemable_jamtrack = true user.is_a_student = !!student user.is_a_teacher = !!teacher + user.retailer_interest = !!retailer_interest user.school_interest = !!school_interest + user.education_interest = !!education_interest if user.is_a_student || user.is_a_teacher musician = true end @@ -1185,10 +1219,18 @@ module JamRuby 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: "Empty biography", school_id: school_id) user.affiliate_referral = school.affiliate_partner end + elsif retailer_id.present? + if user.is_a_student + user.retailer_id = school_id + user.affiliate_referral = retailer.affiliate_partner + elsif user.is_a_teacher + retailer = Retailer.find_by_id(retailer_id) + user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography", retailer_id: retailer_id) + user.affiliate_referral = retailer.affiliate_partner + end else if user.is_a_teacher user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography") @@ -1304,9 +1346,18 @@ module JamRuby # if a gift card value was passed in, then try to find that gift card and apply it to user if gift_card - user.expecting_gift_card = true - found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first - user.gift_cards << found_gift_card if found_gift_card + + # first try posa card + posa_card = PosaCard.where(code: gift_card).first + + if posa_card + posa_card.claim(user) + user.posa_cards << posa_card + else + user.expecting_gift_card = true + found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first + user.gift_cards << found_gift_card if found_gift_card + end end user.save @@ -1382,11 +1433,19 @@ module JamRuby user.handle_test_drive_package(test_drive_package, test_drive_package_details) if test_drive_package if user.is_a_student - UserMailer.student_welcome_message(user).deliver_now + #if school && school.education + # UserMailer.student_education_welcome_message(user).deliver_now + #else + UserMailer.student_welcome_message(user).deliver_now + #end elsif user.is_a_teacher UserMailer.teacher_welcome_message(user).deliver_now + elsif user.education_interest + UserMailer.education_owner_welcome_message(user).deliver_now elsif user.school_interest UserMailer.school_owner_welcome_message(user).deliver_now + elsif user.retailer_interest + UserMailer.retailer_owner_welcome_message(user).deliver_now else UserMailer.welcome_message(user).deliver_now end @@ -1961,6 +2020,11 @@ module JamRuby lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 end + # validate if within waiting period + def can_claim_posa_card + posa_cards.where('card_type = ?', PosaCard::JAM_CLASS_4).where('claimed_at > ?', APP_CONFIG.jam_class_card_wait_period_year.years.ago).count == 0 + end + def lessons_with_teacher(teacher) taken_lessons.where(teacher_id: teacher.id) end @@ -1973,6 +2037,10 @@ module JamRuby remaining_test_drives > 0 end + def has_posa_credits? + jamclass_credits > 0 + end + def has_unprocessed_test_drives? !unprocessed_test_drive.nil? end @@ -2016,6 +2084,24 @@ module JamRuby customer end + ## !!!! this is only valid for tests + def stripe_account_id=(new_acct_id) + existing = stripe_auth + existing.destroy if existing + + user_auth_hash = { + :provider => 'stripe_connect', + :uid => new_acct_id, + :token => 'bogus', + :refresh_token => 'refresh_bogus', + :token_expiration => Date.new(2050, 1, 1), + :secret => "secret" + } + + authorization = user_authorizations.build(user_auth_hash) + authorization.save! + end + def card_approved(token, zip, booking_id, test_drive_package_choice_id = nil) approved_booking = nil @@ -2137,6 +2223,10 @@ module JamRuby LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first end + def most_recent_posa_purchase + lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('posa_card_id is not null').order('created_at desc').first + end + def most_recent_test_drive_purchase lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).order('created_at desc').first end @@ -2150,8 +2240,18 @@ module JamRuby end end + + def total_posa_credits + purchase = most_recent_posa_purchase + if purchase + purchase.posa_card.credits + else + 0 + end + end + def test_drive_succeeded(lesson_session) - if self.remaining_test_drives <= 0 + if (lesson_session.posa_card && self.jamclass_credits <= 0) || (!lesson_session.posa_card && self.remaining_test_drives <= 0) UserMailer.student_test_drive_lesson_done(lesson_session).deliver_now UserMailer.teacher_lesson_completed(lesson_session).deliver_now else @@ -2163,7 +2263,13 @@ module JamRuby def test_drive_declined(lesson_session) # because we decrement test_drive credits as soon as you book, we need to bring it back now if lesson_session.lesson_booking.user_decremented - self.remaining_test_drives = self.remaining_test_drives + 1 + if lesson_session.posa_card + self.jamclass_credits = self.jamclass_credits + 1 + else + self.remaining_test_drives = self.remaining_test_drives + 1 + + end + self.save(validate: false) end @@ -2173,7 +2279,12 @@ module JamRuby if lesson_session.lesson_booking.user_decremented # because we decrement test_drive credits as soon as you book, we need to bring it back now - self.remaining_test_drives = self.remaining_test_drives + 1 + if lesson_session.posa_card + self.jamclass_credits = self.jamclass_credits + 1 + else + self.remaining_test_drives = self.remaining_test_drives + 1 + end + self.save(validate: false) end UserMailer.teacher_test_drive_no_bill(lesson_session).deliver_now @@ -2184,6 +2295,10 @@ module JamRuby total_test_drives - remaining_test_drives end + def used_posa_credits + total_posa_credits - jamclass_credits + end + def uncollectables(limit = 10) LessonPaymentCharge.where(user_id:self.id).order(:created_at).where('billing_attempts > 0').where(billed: false).limit(limit) end @@ -2244,6 +2359,10 @@ module JamRuby LessonBooking.engaged_bookings(student, self, since_at).test_drive.count > 0 end + def same_school_with_student?(student) + student.school && self.teacher && self.teacher.school && student.school.id == self.teacher.school.id + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/models/user_whitelist.rb b/ruby/lib/jam_ruby/models/user_whitelist.rb index d8f496939..7ed8183f6 100644 --- a/ruby/lib/jam_ruby/models/user_whitelist.rb +++ b/ruby/lib/jam_ruby/models/user_whitelist.rb @@ -22,6 +22,14 @@ module JamRuby APP_CONFIG.admin_root_url + "/admin/user_whitelists/" + id end + # if a user claims a gift card or posa card, whitelist their account so they don't get messed with by fraud code + def self.card_create(user, notes) + user_whitelist = UserWhitelist.new + user_whitelist.user = user + user_whitelist.notes = notes + user_whitelist.save + end + def to_s user end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 2c442e074..59803b48d 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -898,6 +898,19 @@ FactoryGirl.define do association :user, factory: :user end + factory :posa_card, class: 'JamRuby::PosaCard' do + sequence(:code) { |n| n.to_s } + card_type JamRuby::PosaCardType::JAM_TRACKS_5 + end + + factory :posa_card_type, class: 'JamRuby::PosaCardType' do + card_type JamRuby::PosaCardType::JAM_TRACKS_5 + end + + factory :posa_card_purchase, class: 'JamRuby::PosaCardPurchase' do + association :user, factory: :user + end + factory :jamblaster, class: 'JamRuby::Jamblaster' do association :user, factory: :user @@ -932,6 +945,22 @@ FactoryGirl.define do accepted false end + factory :retailer, class: 'JamRuby::Retailer' do + association :user, factory: :user + sequence(:name) { |n| "Dat Music Retailer" } + sequence(:slug) { |n| "retailer-#{n}" } + enabled true + end + + factory :retailer_invitation, class: 'JamRuby::RetailerInvitation' do + association :retailer, factory: :retailer + note "hey come in in" + sequence(:email) { |n| "retail_person#{n}@example.com" } + sequence(:first_name) { |n| "FirstName" } + sequence(:last_name) { |n| "LastName" } + accepted false + end + factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do factory :lesson_booking_slot_single do slot_type 'single' diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb index 1f8ca34a1..7bddf1f36 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -35,6 +35,8 @@ describe "Monthly Recurring Lesson Flow" do booking.card_presumed_ok.should be_false booking.user.should eql user booking.card_presumed_ok.should be_false + booking.same_school.should be_false + booking.same_school_free.should be_false booking.should eql user.unprocessed_normal_lesson booking.sent_notices.should be_false booking.booked_price.should eql 30.00 @@ -175,7 +177,6 @@ describe "Monthly Recurring Lesson Flow" do user.reload user.lesson_purchases.length.should eql 1 lesson_purchase = user.lesson_purchases[0] - puts "LESSON_PURCHASE PRICE #{lesson_purchase.price}" lesson_purchase.price.should eql prorated lesson_purchase.lesson_package_type.is_normal?.should eql true lesson_purchase.price_in_cents.should eql prorated_cents @@ -213,6 +214,253 @@ describe "Monthly Recurring Lesson Flow" do + # 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 = 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.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 + end + + it "works (school on school education)" do + + # make sure teacher can get payments + teacher.stripe_account_id = stripe_account1_id + school.user.stripe_account_id = stripe_account2_id + + # get user and teacher into same school + school.education = true + school.save! + 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.same_school.should be_true + booking.same_school_free.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking.reload + booking.card_presumed_ok.should be_true + booking.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 + lesson.reload + + user.reload + user.stripe_customer_id.should_not 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 + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_recurring, 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_recurring, 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, accepter: teacher_user}) + 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 = 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 + + booked_price = booking.booked_price + prorated = booked_price / 2 + prorated_cents = (booked_price * 100).to_i + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql prorated + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql prorated_cents + teacher_distribution = lesson_purchase.teacher_distribution + teacher_distribution.amount_in_cents.should eql prorated_cents + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + education_distribution = lesson_purchase.education_distribution + education_distribution.amount_in_cents.should eql (prorated_cents * 0.0625).round + education_distribution.ready.should be_true + education_distribution.distributed.should be_false + 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 * prorated * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((prorated * 100 * 0.0825).round + 100 * prorated).to_i + sale.recurly_subtotal_in_cents.should eql prorated_cents + 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 + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + teacher_distribution.reload + teacher_distribution.distributed.should be_true + TeacherPayment.count.should eql 2 + payment = teacher_distribution.teacher_payment + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + education_distribution.reload + education_distribution.distributed.should be_true + + education_amt = (3000 * 0.0625).round + payment = education_distribution.teacher_payment + payment.amount_in_cents.should eql education_amt + payment.fee_in_cents.should eql 0 + payment.teacher_payment_charge.amount_in_cents.should eql (education_amt + education_amt * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql education_distribution + + + # teacher & student get into session start = lesson_session.scheduled_start end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) @@ -422,6 +670,7 @@ describe "Monthly Recurring Lesson Flow" do end + it "affiliate gets their cut" do Timecop.travel(2016, 05, 15) user.affiliate_referral = affiliate_partner diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb index da9a65fe7..382001519 100644 --- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -252,7 +252,7 @@ describe "Normal Lesson Flow" do payment = TeacherPayment.first payment.amount_in_cents.should eql 3000 payment.fee_in_cents.should eql (3000 * 0.28).round - payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((3000 * 0.72) + (3000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round payment.teacher.should eql teacher_user payment.teacher_distribution.should eql teacher_distribution @@ -266,12 +266,16 @@ describe "Normal Lesson Flow" do it "works" do + # set up teacher stripe acct + teacher.stripe_account_id = stripe_account1_id + # user has no test drives, no credit card on file, but attempts to book a lesson booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 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.same_school_free.should be_false booking.should eql user.unprocessed_normal_lesson booking.sent_notices.should be_false booking.booked_price.should eql 30.00 @@ -293,6 +297,7 @@ describe "Normal Lesson Flow" do user.stripe_customer_id.should_not be nil user.remaining_test_drives.should eql 0 user.lesson_purchases.length.should eql 0 + teacher_user.stripe_auth.should_not be_nil customer = Stripe::Customer.retrieve(user.stripe_customer_id) customer.email.should eql user.email @@ -389,6 +394,7 @@ describe "Normal Lesson Flow" do UserMailer.deliveries.clear # background code comes around and analyses the session LessonSession.hourly_check + lesson_session.reload lesson_session.analysed.should be_true analysis = lesson_session.analysis @@ -397,6 +403,17 @@ 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 + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 1 + + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + education_distribution = lesson_session.education_distribution + education_distribution.should be_nil lesson_session.billed.should be true user.reload user.lesson_purchases.length.should eql 1 @@ -423,7 +440,7 @@ describe "Normal Lesson Flow" do lesson_session.sent_billing_notices.should be true 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 3 # one for student, one for teacher end @@ -568,6 +585,225 @@ describe "Normal Lesson Flow" do TeacherDistribution.count.should eql 0 end + + it "works (school on school education)" do + + # make sure teacher can get payments + teacher.stripe_account_id = stripe_account1_id + school.user.stripe_account_id = stripe_account2_id + + # make sure can get stripe payments + + # get user and teacher into same school + + school.education = true + school.save! + user.school = school + user.save! + teacher.school = school + teacher.save! + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.school.should be_true + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.same_school_free.should be_true + user.unprocessed_normal_lesson.should be_nil + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + booking.is_requested?.should be_true + booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + LessonPaymentCharge.count.should eql 1 + + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not 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 + 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.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) + 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, accepter: teacher_user}) + 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) + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be_true + 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.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 1 + + LessonBooking.hourly_check + + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + lesson_session.teacher_distributions.count.should eql 2 + education_distribution = lesson_session.education_distribution + education_distribution.amount_in_cents.should eql (3000 * 0.0625).round + education_distribution.ready.should be_true + education_distribution.distributed.should be_false + + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + user.sales.length.should eql 1 + lesson_session.amount_charged.should eql 32.48 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be_true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 2 + + LessonPaymentCharge.count.should eql 1 + TeacherDistribution.count.should eql 2 + + + teacher_distribution.reload + teacher_distribution.distributed.should be_true + education_distribution.reload + education_distribution.distributed.should be_true + + education_amt = (3000 * 0.0625).round + payment = education_distribution.teacher_payment + payment.amount_in_cents.should eql education_amt + payment.fee_in_cents.should eql 0 + payment.teacher_payment_charge.amount_in_cents.should eql (education_amt + education_amt * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql 0 + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql education_distribution + payment = teacher_distribution.teacher_payment + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true + + + + end + it "affiliate gets their cut" do user.affiliate_referral = affiliate_partner user.save! diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb index 28bc1fb40..137001019 100644 --- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -16,7 +16,8 @@ describe "TestDrive Lesson Flow" do let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } let(:school) { FactoryGirl.create(:school) } - + let(:card_lessons) {FactoryGirl.create(:posa_card, card_type: JamRuby::PosaCardType::JAM_CLASS_4)} + let(:retailer) {FactoryGirl.create(:retailer)} before { teacher.stripe_account_id = stripe_account1_id @@ -250,8 +251,208 @@ describe "TestDrive Lesson Flow" do LessonBooking.bookings(user, teacher_user, nil).count.should eql 1 LessonBooking.engaged_bookings(user, teacher_user, nil).count.should eql 1 teacher_user.has_booked_test_drive_with_student?(user).should be_true + end + + it "works using posa card" do + + PosaCard.activate(card_lessons, retailer) + card_lessons.reload + card_lessons.claim(user) + card_lessons.errors.any?.should be false + + user.reload + user.jamclass_credits.should eql 4 + # 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.") + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.sent_notices.should be_true + booking.posa_card.should eql card_lessons + user.unprocessed_test_drive.should be_nil + teacher_user.has_booked_test_drive_with_student?(user).should be_true + + user.reload + user.jamclass_credits.should eql 3 + lesson_session = booking.lesson_sessions[0] + lesson_session.posa_card.should eql card_lessons + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson_session.reload + + #booking.lesson_package_purchases.should eql [card_lessons.lesson_package_purchase] + user.stripe_customer_id.should be nil + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + lesson_purchase.posa_card.should eql card_lessons + 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.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) + 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, accepter: teacher_user}) + 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 be_nil + + + # 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) + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + lesson_session.analyse + lesson_session.session_completed + lesson_session.reload + lesson_session.analysed.should be_true + analysis = lesson_session.analysis + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be false + 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.billing_error_reason.should be_nil + lesson_session.sent_notices.should be true + purchase = lesson_session.lesson_package_purchase + purchase.should_not be_nil + purchase.price_in_cents.should eql 4999 + purchase.lesson_package_type.is_test_drive?.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + found_student_email = false + UserMailer.deliveries.each do |d| + puts d.subject + if d.subject == "You have used 1 of 4 TestDrive lesson credits" + found_student_email = true + end + end + found_student_email.should be_true + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + LessonBooking.hourly_check + LessonSession.hourly_check + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + TeacherPayment.count.should eql 0 + TeacherPayment.hourly_check + TeacherPayment.count.should eql 1 + + lesson_session.reload + purchase.reload + + purchase.teacher_distribution.should be_nil + + teacher_payment = TeacherPayment.first + teacher_payment.amount_in_cents.should eql 1000 + teacher_payment.fee_in_cents.should eql 0 + teacher_payment.teacher.should eql teacher_user + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + + teacher_payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + 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 + + lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED + lesson_session.lesson_booking.success.should be_true + LessonBooking.bookings(user, teacher_user, nil).count.should eql 1 + LessonBooking.engaged_bookings(user, teacher_user, nil).count.should eql 1 + teacher_user.has_booked_test_drive_with_student?(user).should be_true end # VRFS-4069 diff --git a/ruby/spec/jam_ruby/models/posa_card_spec.rb b/ruby/spec/jam_ruby/models/posa_card_spec.rb new file mode 100644 index 000000000..6cd4eed7c --- /dev/null +++ b/ruby/spec/jam_ruby/models/posa_card_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +describe PosaCard do + + let(:user) {FactoryGirl.create(:user)} + let(:card) {FactoryGirl.create(:posa_card)} + let(:card2) {FactoryGirl.create(:posa_card)} + let(:card_lessons) {FactoryGirl.create(:posa_card, card_type: JamRuby::PosaCardType::JAM_CLASS_4)} + let(:retailer) {FactoryGirl.create(:retailer)} + it "created by factory" do + card.touch + end + + describe "activated" do + it "succeeds" do + card.activate(retailer) + + card.errors.any?.should be false + card.activated_at.should_not be_nil + card.retailer.should eql retailer + end + + it "cant be re-activated" do + + card.activate(retailer) + + Timecop.travel(Time.now + 1) + + card.activate(retailer) + + card.errors.any?.should be true + card.errors[:activated_at].should eql ['already activated. Please give card to customer. Thank you!'] + end + + it "must have retailer" do + + card.activate(nil) + + card.errors.any?.should be true + card.errors[:retailer].should eql ['must be specified'] + end + end + + describe "claim" do + it "succeeds" do + PosaCard.activate(card, retailer) + card.reload + card.claim(user) + + card.errors.any?.should be false + card.claimed_at.should_not be_nil + card.user.should eql user + end + + it "succeeds with jamclass type" do + PosaCard.activate(card_lessons, retailer) + card_lessons.reload + card_lessons.claim(user) + + card_lessons.errors.any?.should be false + card_lessons.claimed_at.should_not be_nil + card_lessons.user.should eql user + card_lessons.reload + card_lessons.lesson_package_purchase.should_not be_nil + card_lessons.lesson_package_purchase.lesson_package_type.should eql LessonPackageType.test_drive_4 + card_lessons.lesson_package_purchase.posa_card.should eql card_lessons + end + + + it "must be already activated" do + + card.claim(user) + + card.errors.any?.should be true + card.errors[:activated_at].should eql ['must already be set'] + end + + it "cant be re-claimed" do + + PosaCard.activate(card, retailer) + card.reload + card.claim(user) + + Timecop.travel(Time.now + 1) + + card.claim(user) + + card.errors.any?.should be true + card.errors[:claimed_at].should eql ['already claimed'] + end + + it "must have user" do + PosaCard.activate(card, retailer) + card.reload + card.claim(nil) + + card.errors.any?.should be true + card.errors[:user].should eql ['must be specified'] + end + + it "can't be within one year" do + PosaCard.activate(card, retailer) + card.reload + card.claim(user) + + PosaCard.activate(card2, retailer) + card2.reload + card2.claim(user) + + card2.errors.any?.should be true + card2.errors[:user].should eql ['was within 1 year'] + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb b/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb new file mode 100644 index 000000000..4e1cee8f3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/retailer_invitation_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RetailerInvitation do + + let(:retailer) {FactoryGirl.create(:retailer)} + + it "created by factory" do + FactoryGirl.create(:retailer_invitation) + end + + it "created by method" do + RetailerInvitation.create(retailer.user, retailer, {first_name: "Bobby", last_name: "Jimes", email: 'somewhere@jamkazam.com'}) + end + + describe "index" do + it "works" do + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + + FactoryGirl.create(:retailer_invitation) + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + RetailerInvitation.index(retailer, {})[:query].count.should eql 0 + + FactoryGirl.create(:retailer_invitation, retailer: retailer, ) + RetailerInvitation.index(retailer, {})[:query].count.should eql 1 + + FactoryGirl.create(:retailer_invitation, retailer: retailer, ) + RetailerInvitation.index(retailer, {})[:query].count.should eql 2 + + end + + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/retailer_spec.rb b/ruby/spec/jam_ruby/models/retailer_spec.rb new file mode 100644 index 000000000..b0ca83c27 --- /dev/null +++ b/ruby/spec/jam_ruby/models/retailer_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Retailer do + + it "created by factory" do + FactoryGirl.create(:retailer) + end + + it "doesn't match uuid password" do + retailer= FactoryGirl.create(:retailer) + retailer.reload + retailer.matches_password('hha').should be false + end + + it "automatic slug creation" do + retailer= FactoryGirl.create(:retailer, slug: nil) + retailer.id.should_not be_blank + retailer.slug.should eql retailer.id.to_s + + end + it "has correct associations" do + retailer = FactoryGirl.create(:retailer) + retailer.slug.should eql retailer.id + + retailer.should eql retailer.user.owned_retailer + + teacher = FactoryGirl.create(:teacher, retailer: retailer) + + retailer.reload + retailer.teachers.to_a.should eql [teacher] + + teacher.retailer.should eql retailer + end + + it "updates" do + retailer = FactoryGirl.create(:retailer) + retailer.update_from_params({name: 'hahah'}) + retailer.errors.any?.should be false + end + + it "updates password" do + retailer = FactoryGirl.create(:retailer) + retailer.update_from_params({name: 'hahah', password: 'abc'}) + retailer.errors.any?.should be true + retailer.errors[:password].should eql ['is too short (minimum is 6 characters)'] + + retailer.update_from_params({name: 'hahah', password: 'abcdef'}) + retailer.errors.any?.should be false + retailer.matches_password('abcdef').should be true + + retailer = Retailer.find_by_id(retailer.id) + retailer.matches_password('abcdef').should be true + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 1a68e9910..a0d8077d3 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -8,6 +8,8 @@ describe Sale do let(:jam_track2) { FactoryGirl.create(:jam_track) } let(:jam_track3) { FactoryGirl.create(:jam_track) } let(:gift_card) { GiftCardType.jam_track_5 } + let(:posa_card) {FactoryGirl.create(:posa_card)} + let(:retailer) {FactoryGirl.create(:retailer)} after(:each) { Timecop.return @@ -27,6 +29,39 @@ describe Sale do sale_line_item.product_id.should eq(jamtrack.id) end + describe "posa_cards" do + it "works" do + posa_card.card_type.should eql PosaCard::JAM_TRACKS_5 + + result = Sale.posa_activate(posa_card, retailer) + posa_card.errors.any?.should be false + posa_card.activated_at.should_not be_nil + sale = result[:sale] + + sale = Sale.find(sale.id) + sale.sale_line_items.count.should eql 1 + sale_line_item = sale.sale_line_items.first + sale.retailer.should eql retailer + sale.sale_type.should eql Sale::POSA_SALE + sale_line_item.retailer.should eql retailer + sale_line_item.unit_price.should eql 9.99 # + sale_line_item.quantity.should eql 1 + end + + it "already activated" do + result = Sale.posa_activate(posa_card, retailer) + posa_card.activated_at.should_not be_nil + posa_card.errors.any?.should be false + sale = result[:sale] + + result2 = Sale.posa_activate(posa_card, retailer) + posa_card.activated_at.should_not be_nil + posa_card.errors.any?.should be true + result2[:sale].should be_nil + posa_card.errors[:activated_at].should eq ["already activated. Please give card to customer. Thank you!"] + end + end + describe "index" do it "empty" do result = Sale.index(user) @@ -151,6 +186,7 @@ describe Sale do purchase.state.should eq('invoiced') purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + sleep 2 invoices = recurly_account.invoices invoices.should have(1).items invoice = invoices[0] @@ -454,6 +490,7 @@ describe Sale do purchase.state.should eq('invoiced') purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + sleep 2 invoices = recurly_account.invoices invoices.should have(1).items invoice = invoices[0] @@ -533,6 +570,7 @@ describe Sale do purchase.state.should eq('invoiced') purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + sleep 2 invoices = recurly_account.invoices invoices.should have(1).items invoice = invoices[0] @@ -941,6 +979,7 @@ describe Sale do r.voided.to_i.should eq(1) end + end end diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb index 446d9e01e..4a68ff207 100644 --- a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -167,14 +167,14 @@ describe TeacherPayment do # only one confirm email to teacher UserMailer.deliveries.length.should eql 1 payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 payment.teacher_payment_charge.teacher.should eql teacher teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round - charge.application_fee.should include("fee_") + charge.amount.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should be_nil end @@ -199,13 +199,13 @@ describe TeacherPayment do puts payment.teacher_payment_charge.billing_error_detail end payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round - charge.application_fee.should include("fee_") + charge.amount.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round + charge.application_fee.should be_nil test_drive_distribution.reload payment = test_drive_distribution.teacher_payment @@ -220,7 +220,7 @@ describe TeacherPayment do teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.amount.should eql 1000 + (1000 * APP_CONFIG.stripe[:ach_pct]).round charge.application_fee.should be_nil end @@ -259,15 +259,15 @@ describe TeacherPayment do # one to school owner, one to teacher UserMailer.deliveries.length.should eql 2 payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 payment.teacher_payment_charge.user.should eql school.owner teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.destination.should eql school.owner.teacher.stripe_account_id - charge.amount.should eql 1008 - charge.application_fee.should include("fee_") + charge.destination.should be_nil + charge.amount.should eql 726 + charge.application_fee.should be_nil end end @@ -304,7 +304,7 @@ describe TeacherPayment do payment.teacher_payment_charge.billing_error_detail.should include("declined") payment.teacher_payment_charge.billed.should eql false - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 @@ -326,7 +326,7 @@ describe TeacherPayment do # no attempt should be made because a day hasn't gone by payment = normal_distribution.teacher_payment payment.teacher_payment_charge.billed.should eql false - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 @@ -347,12 +347,12 @@ describe TeacherPayment do payment = normal_distribution.teacher_payment payment.reload payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql 1008 + charge.amount.should eql 726 end @@ -386,12 +386,12 @@ describe TeacherPayment do payment = normal_distribution.teacher_payment payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.amount.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round test_drive_distribution.reload payment = test_drive_distribution.teacher_payment @@ -402,7 +402,7 @@ describe TeacherPayment do teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + charge.amount.should eql 1000 + (1000 * APP_CONFIG.stripe[:ach_pct]).round end end @@ -434,7 +434,7 @@ describe TeacherPayment do payment.teacher_payment_charge.billing_error_detail.should include("declined") payment.teacher_payment_charge.billed.should eql false - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 @@ -456,7 +456,7 @@ describe TeacherPayment do # no attempt should be made because a day hasn't gone by payment = normal_distribution.teacher_payment payment.teacher_payment_charge.billed.should eql false - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 @@ -477,12 +477,12 @@ describe TeacherPayment do payment = normal_distribution.teacher_payment payment.reload payment.teacher_payment_charge.billed.should eql true - payment.teacher_payment_charge.amount_in_cents.should eql (1000 + 1000 * APP_CONFIG.stripe[:ach_pct]).round + payment.teacher_payment_charge.amount_in_cents.should eql ((1000 * 0.72) + (1000 * 0.72) * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.fee_in_cents.should eql 280 teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution.amount_in_cents.should eql 1000 charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) - charge.amount.should eql 1008 + charge.amount.should eql 726 end end end diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index 2f7cc727c..0ea998699 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -7,6 +7,7 @@ require "spec_helper" describe "RenderMailers", :slow => true do let(:user) { FactoryGirl.create(:user) } + let(:school) {FactoryGirl.create(:school, education:true)} before(:each) do @filename = nil # set this on your test to pin the filename; i just make it the name of the mailer method responsible for sending the mail @@ -27,7 +28,9 @@ describe "RenderMailers", :slow => true do it { @filename="welcome_message"; UserMailer.welcome_message(user).deliver_now } it { @filename="student_welcome_message"; UserMailer.student_welcome_message(user).deliver_now } + it { @filename="student_welcome_message_education"; user.school = school; user.save!; UserMailer.student_welcome_message(user).deliver_now } it { @filename="school_owner_welcome_message"; UserMailer.school_owner_welcome_message(user).deliver_now } + it { @filename="education_owner_welcome_message"; UserMailer.education_owner_welcome_message(user).deliver_now } it { @filename="confirm_email"; UserMailer.confirm_email(user, "/signup").deliver_now } it { @filename="password_reset"; UserMailer.password_reset(user, '/reset_password').deliver_now } it { @filename="password_changed"; UserMailer.password_changed(user).deliver_now } @@ -210,6 +213,22 @@ describe "RenderMailers", :slow => true do end end + describe "Retailer emails" do + let(:retailer) {FactoryGirl.create(:retailer)} + + before(:each) do + UserMailer.deliveries.clear + end + after(:each) do + UserMailer.deliveries.length.should == 1 + # NOTE! we take the second email, because the act of creating the InvitedUser model + # sends an email too, before our it {} block runs. This is because we have an InvitedUserObserver + mail = UserMailer.deliveries[0] + save_emails_to_disk(mail, @filename) + end + it {@filename="retailer_customer_blast"; UserMailer.retailer_customer_blast('seth@jamkazam.com', retailer).deliver_now} + end + describe "InvitedUserMailer emails" do let(:user2) { FactoryGirl.create(:user) } diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index ca203612f..0a4c93c00 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -294,6 +294,10 @@ def app_config 1 end + def jam_class_card_wait_period_year + 1 + end + def check_bounced_emails false end diff --git a/web/app/assets/images/landing/Scott Himel - Avatar.png b/web/app/assets/images/landing/Scott Himel - Avatar.png new file mode 100644 index 000000000..4b8123816 Binary files /dev/null and b/web/app/assets/images/landing/Scott Himel - Avatar.png differ diff --git a/web/app/assets/images/landing/Scott Himel - Speech Bubble.png b/web/app/assets/images/landing/Scott Himel - Speech Bubble.png new file mode 100644 index 000000000..06dc9110e Binary files /dev/null and b/web/app/assets/images/landing/Scott Himel - Speech Bubble.png differ diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 6f144f06a..b7025e1ea 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -76,10 +76,12 @@ isNativeClient: gon.isNativeClient, musician: context.JK.currentUserMusician, sales_count: userDetail.sales_count, + owned_retailer_id: userDetail.owned_retailer_id, is_affiliate_partner: userDetail.is_affiliate_partner, affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), affiliate_referral_count: userDetail.affiliate_referral_count, owns_school: !!userDetail.owned_school_id, + owns_retailer: !!userDetail.owned_retailer_id, webcamName: webcamName } , { variable: 'data' })); @@ -145,6 +147,7 @@ $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } ); $("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } ); $("#account-content-scroller").on('click', '#account-school-link', function(evt) {evt.stopPropagation(); navToSchool(); return false; } ); + $("#account-content-scroller").on('click', '#account-retailer-link', function(evt) {evt.stopPropagation(); navToRetailer(); return false; } ); } function renderAccount() { @@ -207,6 +210,11 @@ window.location = '/client#/account/school' } + function navToRetailer() { + resetForm() + window.location = '/client#/account/retailer' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index c058edb1f..6e4be938e 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -1,3 +1,4 @@ + (function(context,$) { "use strict"; @@ -22,17 +23,17 @@ function afterShow(data) { - if (window.ProfileStore.solo) { - $btnBack.hide() - $btnSubmit.text('SAVE & RETURN TO PROFILE'); - } - else { - $btnBack.show() - $btnSubmit.text('SAVE & NEXT'); - } + if (window.ProfileStore.solo) { + $btnBack.hide() + $btnSubmit.text('SAVE & RETURN TO PROFILE'); + } + else { + $btnBack.show() + $btnSubmit.text('SAVE & NEXT'); + } - resetForm(); - renderExperience(); + resetForm(); + renderExperience(); } function resetForm() { @@ -64,6 +65,7 @@ $screen.find('select[name=skill_level]').val(userDetail.skill_level); $screen.find('select[name=concert_count]').val(userDetail.concert_count); $screen.find('select[name=studio_session_count]').val(userDetail.studio_session_count); + context.JK.checkbox($instrumentSelector.find('input[type="checkbox"]'), true) } function isUserInstrument(instrument, userInstruments) { @@ -101,6 +103,8 @@ }); $userGenres.append(genreHtml); }); + + context.JK.checkbox($userGenres.find('input[type="checkbox"]'), true) }); } @@ -132,7 +136,7 @@ navigateTo('/client#/account/profile/'); return false; }); - + enableSubmits() } @@ -178,9 +182,9 @@ concert_count: $screen.find('select[name=concert_count]').val(), studio_session_count: $screen.find('select[name=studio_session_count]').val() }) - .done(postUpdateProfileSuccess) - .fail(postUpdateProfileFailure) - .always(enableSubmits) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure) + .always(enableSubmits) } function postUpdateProfileSuccess(response) { @@ -216,7 +220,7 @@ instrument_id: instrumentElement.attr('data-instrument-id'), proficiency_level: proficiency, priority : i - }); + }); }); return instruments; @@ -239,4 +243,4 @@ return this; }; -})(window,jQuery); \ No newline at end of file +})(window,jQuery); diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 286214c4d..851526ca4 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -181,6 +181,7 @@ } function initializeInfluxDB() { + /** context.stats = new InfluxDB({ "host" : gon.global.influxdb_host, "port" : gon.global.influxdb_port, @@ -190,6 +191,8 @@ }); context.stats.write = context.stats.writePoint; + */ + context.stats = {write:function() {}} } function initializeStun(app) { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index ea23a8f73..ae5713c90 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -199,6 +199,17 @@ }) }}) } + helpBubble.showUseRemainingJamClassCreditsBubble = function($element, $offsetParent, user, callback) { + return context.JK.onceBubble($element, 'side-remaining-jamclass-credits', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { + + var $bookNow = $('a.book-now') + $bookNow.off('click').on('click', function(e) { + e.preventDefault() + callback() + return false; + }) + }}) + } helpBubble.showBuyTestDrive = function($element, $offsetParent, user, callback) { return context.JK.onceBubble($element, 'side-buy-test-drive', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js index a745e1b65..cf4cd9372 100644 --- a/web/app/assets/javascripts/homeScreen.js +++ b/web/app/assets/javascripts/homeScreen.js @@ -9,6 +9,7 @@ var $screen = null; function beforeShow(data) { + } function afterShow(data) { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 6475b0ff5..4e74551d1 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2367,8 +2367,6 @@ }) } - - function updateSchoolAvatar(options) { var id = getId(options); @@ -2488,6 +2486,171 @@ }); } + function deleteSchoolTeacher(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/schools/" + id + '/teachers/' + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function getRetailer(options) { + + var id = getId(options); + return $.ajax({ + type: "GET", + url: "/api/retailers/" + id, + dataType: "json", + contentType: 'application/json' + }); + } + + function updateRetailer(options) { + var id = getId(options); + return $.ajax({ + type: "POST", + url: '/api/retailers/' + id, + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function updateRetailerAvatar(options) { + var id = getId(options); + + var original_fpfile = options['original_fpfile']; + var cropped_fpfile = options['cropped_fpfile']; + var cropped_large_fpfile = options['cropped_large_fpfile']; + var crop_selection = options['crop_selection']; + + logger.debug(JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + })); + + var url = "/api/retailers/" + id + "/avatar"; + return $.ajax({ + type: "POST", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + cropped_large_fpfile : cropped_large_fpfile, + crop_selection : crop_selection + }) + }); + } + + function deleteRetailerAvatar(options) { + var id = getId(options); + + var url = "/api/retailers/" + id + "/avatar"; + return $.ajax({ + type: "DELETE", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false + }); + } + + function generateRetailerFilePickerPolicy(options) { + var id = getId(options); + var handle = options && options["handle"]; + var convert = options && options["convert"] + + var url = "/api/retailers/" + id + "/filepicker_policy"; + + return $.ajax(url, { + data : { handle : handle, convert: convert }, + dataType : 'json' + }); + } + + function listRetailerInvitations(options) { + + var id = getId(options); + + return $.ajax({ + type: "GET", + url: "/api/retailers/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json' + }); + } + + function createRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/retailers/" + id + '/invitations?' + $.param(options) , + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/invitations/' + options.invitation_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function resendRetailerInvitation(options) { + + var id = getId(options); + + return $.ajax({ + type: "POST", + url: "/api/retaiers/" + id + '/invitations/' + options.invitation_id + '/resend', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + + function deleteRetailerStudent(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/students/' + options.student_id, + dataType: "json", + contentType: 'application/json' + }); + } + + function deleteRetailerTeacher(options) { + + var id = getId(options); + + return $.ajax({ + type: "DELETE", + url: "/api/retailers/" + id + '/teachers/' + options.teacher_id, + dataType: "json", + contentType: 'application/json' + }); + } + function listTeacherDistributions(options) { if(!options) { @@ -2502,18 +2665,6 @@ }); } - function deleteSchoolTeacher(options) { - - var id = getId(options); - - return $.ajax({ - type: "DELETE", - url: "/api/schools/" + id + '/teachers/' + options.teacher_id, - dataType: "json", - contentType: 'application/json' - }); - } - function createReview(options) { return $.ajax({ @@ -2545,6 +2696,45 @@ }) } + function posaActivate(options) { + var slug = options.slug + delete options.slug + + return $.ajax({ + type: "POST", + url: '/api/posa/' + slug + '/activate', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options), + }) + } + + function posaClaim(options) { + + return $.ajax({ + type: "POST", + url: '/api/posa/claim', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options), + }) + } + + function sendRetailerCustomerEmail(options) { + options = options || {} + var retailerId = options.retailer + delete options.retailer + + return $.ajax({ + type: 'POST', + url: '/api/retailers/' + retailerId + '/customer_email', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function initialize() { return self; } @@ -2768,11 +2958,25 @@ this.resendSchoolInvitation = resendSchoolInvitation; this.deleteSchoolTeacher = deleteSchoolTeacher; this.deleteSchoolStudent = deleteSchoolStudent; + this.getRetailer = getRetailer; + this.updateRetailer = updateRetailer; + this.updateRetailerAvatar = updateRetailerAvatar; + this.deleteRetailerAvatar = deleteRetailerAvatar; + this.generateRetailerFilePickerPolicy = generateRetailerFilePickerPolicy; + this.listRetailerInvitations = listRetailerInvitations; + this.createRetailerInvitation = createRetailerInvitation; + this.deleteRetailerInvitation = deleteRetailerInvitation; + this.resendRetailerInvitation = resendRetailerInvitation; + this.deleteRetailerTeacher = deleteRetailerTeacher; + this.deleteRetailerStudent = deleteRetailerStudent; this.listTeacherDistributions = listTeacherDistributions; this.lessonStartTime = lessonStartTime; this.createReview = createReview; this.askSearchHelp = askSearchHelp; this.ratingDecision = ratingDecision; + this.posaActivate = posaActivate; + this.posaClaim = posaClaim; + this.sendRetailerCustomerEmail = sendRetailerCustomerEmail; return this; }; })(window,jQuery); diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index 6308f8c88..1ad2b3aea 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -298,7 +298,9 @@ } } - monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + if (setTimeout) { + monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + } } function monitorRecordingPlayback() { diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 10a21bb99..8a50c5178 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -8,6 +8,7 @@ //= require ./react-components/stores/UserActivityStore //= require ./react-components/stores/LessonTimerStore //= require ./react-components/stores/SchoolStore +//= require ./react-components/stores/RetailerStore //= require ./react-components/stores/JamBlasterStore //= require ./react-components/stores/StripeStore //= require ./react-components/stores/AvatarStore diff --git a/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee new file mode 100644 index 000000000..e4d31e1be --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountRetailerScreen.js.jsx.coffee @@ -0,0 +1,398 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +LocationActions = context.LocationActions +RetailerActions = context.RetailerActions +RetailerStore = context.RetailerStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountRetailerScreen = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(RetailerStore, "onRetailerChanged") + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + TILE_ACCOUNT: 'account' + TILE_TEACHERS: 'teachers' + TILE_SALES: 'sales' + TILE_AGREEMENT: 'agreement' + + TILES: ['account', 'teachers', 'sales', 'agreement'] + + onAppInit: (@app) -> + @app.bindScreen('account/retailer', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onRetailerChanged: (retailerState) -> + @setState(retailerState) + + onUserChanged: (userState) -> + @noRetailerCheck(userState?.user) + @setState({user: userState?.user}) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}] + @root = $(@getDOMNode()) + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + #@setState({userSchedulingComm: value}) + + + beforeHide: (e) -> + #ProfileActions.viewTeacherProfileDone() + @screenVisible = false + return true + + beforeShow: (e) -> + LocationActions.load() + + noRetailerCheck: (user) -> + if user?.id? && @screenVisible + + if !user.owned_retailer_id? + window.JK.Banner.showAlert("You are not the owner of a retailer in our systems. If you are, please contact support@jamkazam.com and we'll update your account.") + return false + else + if !@shownOnce + @shownOnce = true + RetailerActions.refresh(user.owned_retailer_id) + + return true + + else + return false + + afterShow: (e) -> + @screenVisible = true + logger.debug("AccountRetailerScreen: afterShow") + logger.debug("after show", @state.user) + @noRetailerCheck(@state.user) + + getInitialState: () -> + { + retailer: null, + user: null, + selected: 'account', + updateErrors: null, + retailerName: null, + teacherInvitations: null, + updating: false + } + + nameValue: () -> + if this.state.retailerName? + this.state.retailerName + else + this.state.retailer.name + + nameChanged: (e) -> + $target = $(e.target) + val = $target.val() + @setState({retailerName: val}) + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + onUpdate: (e) -> + e.preventDefault() + + if this.state.updating + return + name = @root.find('input[name="name"]').val() + region = @root.find('select[name="regions"]').val() + city = @root.find('select[name="cities"]').val() + password = @root.find('input[type="password"]').val() + + @setState(updating: true) + rest.updateRetailer({ + id: this.state.retailer.id, + name: name, + state: region, + city: city, + password:password + + }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + onUpdateDone: (response) -> + @setState({retailer: response, retailerName: null, updateErrors: null, updating: false}) + + @app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"}) + + onUpdateFail: (jqXHR) -> + handled = false + + @setState({updating: false}) + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + handled = true + @setState({updateErrors: errors}) + + + if !handled + @app.ajaxError(jqXHR, null, null) + + inviteTeacher: () -> + @app.layout.showDialog('invite-retailer-user', {d1: true}) + + resendInvitation: (id, e) -> + e.preventDefault() + rest.resendRetailerInvitation({ + id: this.state.retailer.id, invitation_id: id + }).done((response) => @resendInvitationDone(response)).fail((jqXHR) => @resendInvitationFail(jqXHR)) + + resendInvitationDone: (response) -> + @app.layout.notify({title: 'invitation resent', text: 'Invitation was resent to ' + response.email}) + + resendInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + deleteInvitation: (id, e) -> + e.preventDefault() + rest.deleteRetailerInvitation({ + id: this.state.retailer.id, invitation_id: id + }).done((response) => @deleteInvitationDone(id, response)).fail((jqXHR) => @deleteInvitationFail(jqXHR)) + + deleteInvitationDone: (id, response) -> + context.RetailerActions.deleteInvitation(id) + + deleteInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + removeFromRetailer: (id, isTeacher, e) -> + if isTeacher + rest.deleteRetailerTeacher({id: this.state.retailer.id, teacher_id: id}).done((response) => @removeFromRetailerDone(response)).fail((jqXHR) => @removeFromRetailerFail(jqXHR)) + + removeFromRetailerDone: (retailer) -> + context.JK.Banner.showNotice("User removed", "User was removed from your retailer.") + context.RetailerActions.updateRetailer(retailer) + + removeFromRetailerFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + renderUser: (user, isTeacher) -> + photo_url = user.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + `
+
+ +
+
+ {user.name} +
+
+ remove from retailer +
+
` + + + renderInvitation: (invitation) -> + `
+ + + + + +
{invitation.first_name} {invitation.last_name} +
has not yet accepted invitation
+ resend invitation + delete +
+ +
+
` + + renderTeachers: () -> + teachers = [] + + if this.state.retailer.teachers? && this.state.retailer.teachers.length > 0 + for teacher in this.state.retailer.teachers + if teacher.user + teachers.push(@renderUser(teacher.user, true)) + else + teachers = `

No teachers

` + + teachers + + renderTeacherInvitations: () -> + invitations = [] + + if this.state.teacherInvitations? && this.state.teacherInvitations.length > 0 + for invitation in this.state.teacherInvitations + invitations.push(@renderInvitation(invitation)) + else + invitations = `

No pending invitations

` + invitations + + mainContent: () -> + if !@state.user? || !@state.retailer? + `
Loading...
` + else if @state.selected == @TILE_ACCOUNT + @account() + else if @state.selected == @TILE_TEACHERS + @teachers() + else if @state.selected == @TILE_SALES + @earnings() + else if @state.selected == @TILE_AGREEMENT + @agreement() + else + @account() + + handleLocationChange: (country, region, city) -> + logger.debug("handleLocationChange #{country} #{region} ${city}") + @setState({city: city, region: region}) + + account: () -> + + nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) + correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors) + nameClasses = classNames({name: true, error: nameErrors?, field: true}) + + cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } + updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + + processUrl = context.JK.makeAbsolute("/posa/#{this.state.retailer.slug}") + processSaleUrl = `{processUrl}` + + `
+
+ + + {nameErrors} +
+
+ + +
+ + + + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + {processSaleUrl} (enter Administrator/password to access this page) +
+
+ +

Payments

+ +
+ +
+ +
+ CANCEL + UPDATE +
+
` + + + teachers: () -> + teachers = @renderTeachers() + teacherInvitations = @renderTeacherInvitations() + + `
+
+
+

teachers:

+ INVITE TEACHER +
+
+
+ {teacherInvitations} +
+ +
+ {teachers} +
+
+
` + + earnings: () -> + `
+

Coming soon

+
` + + agreement: () -> + `
+

The agreement between your retailer and JamKazam is part of JamKazam's terms of service. You can find the + complete terms of service here. And you can find the section that is + most specific to the retailer terms here.

+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @setState({selected: selection}) + + createTileLink: (i, tile) -> + active = this.state.selected == tile + classes = classNames({last: i == @TILES.length - 1, activeTile: active}) + + return `
{tile}
` + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @TILES + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNav = `
+ {profileSelections} +
` + + `
+
+
retailer:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee index 6d43f38ea..c574abca2 100644 --- a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee @@ -57,6 +57,7 @@ profileUtils = context.JK.ProfileUtils beforeHide: (e) -> #ProfileActions.viewTeacherProfileDone() @screenVisible = false + return true beforeShow: (e) -> @@ -92,7 +93,8 @@ profileUtils = context.JK.ProfileUtils schoolName: null, studentInvitations: null, teacherInvitations: null, - updating: false + updating: false, + distributions: [] } isSchoolManaged: () -> @@ -187,9 +189,15 @@ profileUtils = context.JK.ProfileUtils removeFromSchool: (id, isTeacher, e) -> if isTeacher - rest.deleteSchoolTeacher({id: this.state.school.id, teacher_id: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + rest.deleteSchoolTeacher({ + id: this.state.school.id, + teacher_id: id + }).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) else - rest.deleteSchoolStudent({id: this.state.school.id, student_id: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + rest.deleteSchoolStudent({ + id: this.state.school.id, + student_id: id + }).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) removeFromSchoolDone: (school) -> context.JK.Banner.showNotice("User removed", "User was removed from your school.") @@ -203,12 +211,15 @@ profileUtils = context.JK.ProfileUtils if !photo_url? photo_url = '/assets/shared/avatar_generic.png' + mailto = "mailto:#{user.email}" + `
- +
- {user.name} + {user.name} + {user.email}
remove from school @@ -237,7 +248,8 @@ profileUtils = context.JK.ProfileUtils if this.state.school.teachers? && this.state.school.teachers.length > 0 for teacher in this.state.school.teachers - teachers.push(@renderUser(teacher.user, true)) + if teacher.user + teachers.push(@renderUser(teacher.user, true)) else teachers = `

No teachers

` @@ -302,8 +314,39 @@ profileUtils = context.JK.ProfileUtils field: true }) - cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } - updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + cancelClasses = {"button-grey": true, "cancel": true, disabled: this.state.updating} + updateClasses = {"button-orange": true, "update": true, disabled: this.state.updating} + + if this.state.school.education + management = null + else + management = `
+

Management Preference

+ +
+
+ +
+
+ +
+
+
+ + + +
All emails relating to lesson scheduling will go to this email if school owner manages + scheduling. +
+ {correspondenceEmailErrors} +
+
` + `
@@ -316,29 +359,7 @@ profileUtils = context.JK.ProfileUtils
-

Management Preference

- -
-
- -
-
- -
-
-
- - - -
All emails relating to lesson scheduling will go to this email if school owner manages - scheduling. -
- {correspondenceEmailErrors} -
+ {management}

Payments

@@ -365,7 +386,7 @@ profileUtils = context.JK.ProfileUtils

teachers:

INVITE TEACHER -
+
{teacherInvitations} @@ -397,6 +418,73 @@ profileUtils = context.JK.ProfileUtils

Coming soon

` + paymentsToYou: () -> + rows = [] + + for paymentHistory in this.state.distributions + paymentMethod = 'Stripe' + + if paymentHistory.distributed + date = paymentHistory.teacher_payment.teacher_payment_charge.last_billing_attempt_at + status = 'Paid' + else + date = paymentHistory.created_at + if paymentHistory.not_collectable + status = 'Uncollectible' + else if !paymentHistory.teacher?.teacher?.stripe_account_id? + status = 'No Stripe Acct' + else + status = 'Collecting' + + + date = context.JK.formatDate(date, true) + description = paymentHistory.description + + if paymentHistory.teacher_payment? + amt = paymentHistory.teacher_payment.real_distribution_in_cents + else + amt = paymentHistory.real_distribution_in_cents + + displayAmount = ' $' + (amt / 100).toFixed(2) + + amountClasses = {status: status} + + row = + ` + {date} + {paymentMethod} + {description} + {status} + {displayAmount} + ` + rows.push(row) + + `
+ + + + + + + + + + + + {rows} + + +
DATEMETHODDESCRIPTIONSTATUSAMOUNT
+ Next + +
No more payment history
+
+ BACK +
+
+
` + + agreement: () -> `

The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the diff --git a/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee index 4b08de848..ab4b20b4a 100644 --- a/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee @@ -31,18 +31,28 @@ AvatarStore = context.AvatarStore render: () -> if this.props.target?.photo_url? - testStudentUrl = "/school/#{this.props.target.id}/student?preview=true" - testTeacherUrl = "/school/#{this.props.target.id}/teacher?preview=true" + target_type = this.props.target_type + + testStudentUrl = "/#{target_type}/#{this.props.target.id}/student?preview=true" + testTeacherUrl = "/#{target_type}/#{this.props.target.id}/teacher?preview=true" + + if target_type == 'school' + previewArea = `

See how it will look to  + students and  + teachers +
` + else + previewArea = `
See how it will look to  + teachers +
` + `

change/update logo
-
See how it will look to  - students and  - teachers -
+ {previewArea}
` else `
diff --git a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee index f19dce5ee..ae73ed2c0 100644 --- a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee @@ -99,7 +99,7 @@ UserStore = context.UserStore userDetailDone: (response) -> if response.id == @state.teacherId - school_on_school = response.teacher.school_id? && @state.user?.school_id? && response.teacher.school_id == @state.user.school_id + school_on_school = response.teacher.school_id? && @state.user?.school_id? && response.teacher.school_id == @state.user.school_id && !response.teacher.school.education @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) @@ -234,7 +234,7 @@ UserStore = context.UserStore booked: (response) -> @setState({updating: false}) UserActions.refresh() - if response.user['has_stored_credit_card?'] || @state.school_on_school + if response.user['has_stored_credit_card?'] || @state.school_on_school || response.posa_card_id? context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.

We've taken you back to the JamClass home page, where you can check the status of this lesson, as well as any other past and future lessons.") url = "/client#/jamclass/lesson-booking/#{response.id}" url = "/client#/jamclass" @@ -441,11 +441,15 @@ UserStore = context.UserStore if @isTestDrive() + credits = this.state.user.remaining_test_drives + if this.state.user.jamclass_credits > 0 + credits = this.state.user.jamclass_credits + header = `

book testdrive lesson

` - if @state.user?.remaining_test_drives == 1 + if credits == 1 testDriveLessons = "1 TestDrive lesson credit" else - testDriveLessons = "#{this.state.user.remaining_test_drives} TestDrive lesson credits" + testDriveLessons = "#{credits} TestDrive lesson credits" actions = `
CANCEL @@ -461,7 +465,7 @@ UserStore = context.UserStore else if this.state.user.lesson_package_type_id == 'test-drive-2' testDriveCredits = 2 - if this.state.user.remaining_test_drives > 0 + if credits > 0 testDriveBookingInfo = `

You are booking a single 30-minute TestDrive session.

diff --git a/web/app/assets/javascripts/react-components/InviteRetailerUserDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/InviteRetailerUserDialog.js.jsx.coffee new file mode 100644 index 000000000..0e164c45f --- /dev/null +++ b/web/app/assets/javascripts/react-components/InviteRetailerUserDialog.js.jsx.coffee @@ -0,0 +1,147 @@ +context = window +RetailerStore = context.RetailerStore + +@InviteRetailerUserDialog = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(RetailerStore, "onRetailerChanged")] + teacher: false + + beforeShow: (args) -> + logger.debug("InviteRetailerUserDialog.beforeShow", args.d1) + @firstName = '' + @lastName = '' + @email = '' + + @setState({inviteErrors: null, teacher: args.d1}) + afterHide: () -> + + onRetailerChanged: (retailerState) -> + @setState(retailerState) + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('invite-retailer-user', dialogBindings); + + componentDidMount: () -> + @root = $(@getDOMNode()) + + getInitialState: () -> + {inviteErrors: null, retailer: null, sending: false} + + doCancel: (e) -> + e.preventDefault() + @app.layout.closeDialog('invite-retailer-user', true); + + doInvite: (e) -> + e.preventDefault() + + if this.state.sending + console.log("sending already") + return + + + email = @root.find('input[name="email"]').val() + lastName = @root.find('input[name="last_name"]').val() + firstName = @root.find('input[name="first_name"]').val() + retailer = context.RetailerStore.getState().retailer + @setState({inviteErrors: null, sending: true}) + rest.createRetailerInvitation({ + id: retailer.id, + as_teacher: this.state.teacher, + email: email, + last_name: lastName, + first_name: firstName + }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) + + createDone: (response) -> + console.log("invitation added", response) + @setState({inviteErrors:null, sending: false}) + context.RetailerActions.addInvitation(this.state.teacher, response) + context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!") + @app.layout.closeDialog('invite-retailer-user') + + createFail: (jqXHR) -> + handled = false + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + @setState({inviteErrors: errors, sending: false}) + handled = true + + if !handled + @app.ajaxError(jqXHR, null, null) + + + close: (e) -> + e.preventDefault() + @app.layout.closeDialog('invite-retailer-user'); + + + renderRetailer: () -> + firstNameErrors = context.JK.reactSingleFieldErrors('first_name', @state.inviteErrors) + lastNameErrors = context.JK.reactSingleFieldErrors('last_name', @state.inviteErrors) + emailErrors = context.JK.reactSingleFieldErrors('email', @state.inviteErrors) + + firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true}) + lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true}) + emailClasses = classNames({email: true, error: emailErrors?, field: true}) + sendInvitationClasses = classNames({'button-orange': true, disabled: this.state.sending}) + + if @state.teacher + title = 'invite teacher' + help = `

Send invitations to teachers who teach through your music store. When your teachers accept this invitation to create teacher accounts on JamKazam, you can easily send emails to customers who purchase online lessons pointing these customers to your preferred teachers from your store.

` + else + title = 'invite student' + help = `

+ Shouldn't be here... +

` + + `
+
+ + +

{title}

+
+
+ + {help} + +
+ + + {firstNameErrors} +
+ +
+ + + {lastNameErrors} +
+ +
+ + + {emailErrors} +
+ + +
+
` + + render: () -> + retailer = this.state.retailer + + if !retailer? + return `
no retailer
` + + @renderRetailer() + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee index 06753da19..6352eef89 100644 --- a/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee @@ -1,8 +1,9 @@ context = window +SchoolStore = context.SchoolStore @InviteSchoolUserDialog = React.createClass({ - mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(SchoolStore, "onSchoolChanged")] teacher: false beforeShow: (args) -> @@ -14,6 +15,9 @@ context = window @setState({inviteErrors: null, teacher: args.d1}) afterHide: () -> + onSchoolChanged: (schoolState) -> + @setState(schoolState) + onAppInit: (@app) -> dialogBindings = { 'beforeShow': @beforeShow, @@ -22,12 +26,11 @@ context = window @app.bindDialog('invite-school-user', dialogBindings); - componentDidMount: () -> @root = $(@getDOMNode()) getInitialState: () -> - {inviteErrors: null} + {inviteErrors: null, school: null, sending: false} doCancel: (e) -> e.preventDefault() @@ -36,39 +39,93 @@ context = window doInvite: (e) -> e.preventDefault() + if this.state.sending + console.log("sending already") + return + email = @root.find('input[name="email"]').val() lastName = @root.find('input[name="last_name"]').val() firstName = @root.find('input[name="first_name"]').val() school = context.SchoolStore.getState().school - @setState({inviteErrors: null}) - rest.createSchoolInvitation({id: school.id, as_teacher: this.state.teacher, email: email, last_name: lastName, first_name: firstName }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) + @setState({inviteErrors: null, sending: true}) + rest.createSchoolInvitation({ + id: school.id, + as_teacher: this.state.teacher, + email: email, + last_name: lastName, + first_name: firstName + }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR)) - createDone:(response) -> - context.SchoolActions.addInvitation(@state.teacher, response) + createDone: (response) -> + console.log("invitation added", response) + @setState({inviteErrors:null, sending: false}) + context.SchoolActions.addInvitation(this.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}) + @setState({inviteErrors: errors, sending: false}) handled = true if !handled @app.ajaxError(jqXHR, null, null) - render: () -> + renderEducation: () -> + `
+
+ +

How to Invite Your Students

+
+
+ +

+ Please copy and paste the text below into the email application you use to communicate with students and + parents in your music program. This is a suggested starting point, but you may edit the message as you prefer. + Please make sure the web page link in this message is included in the email you send and is unchanged because + students must use this specific link to sign up so that they will be properly associated with your school. +

+ + + +
+ DONE +
+
+
` + + close: (e) -> + e.preventDefault() + @app.layout.closeDialog('invite-school-user'); + + educationCopyEmailText: () -> + path = context.JK.makeAbsolute("/school/#{this.state.school.id}/student") + + msg = "Hello Students & Parents - + +I'm writing to make you aware of a very interesting new option for private music lessons. A company called JamKazam has built remarkable new technology that lets musicians play together live in sync with studio quality audio from different locations over the Internet. Here's an example: https://www.youtube.com/watch?v=I2reeNKtRjg. Now they have built an online music lesson service that uses this technology: https://www.youtube.com/watch?v=wdMN1fQyD9k. + +\n\nThis means that students can now take lessons online and much more conveniently from home. Parents don't have to leave work early to drive students to and from lessons during rush hour. A 30-minute lesson is just a 30-minute lesson at home, not a 90-minute expedition across town. And students can record lessons to refer back to them later. + +\n\nIf the convenience of online lessons is attractive to your family, then you can use this link to sign up for online lessons: #{path}. After you sign up, someone from JamKazam will reach out to answer your questions and help you get set up and ready to go. Your student can continue to take lessons from the same instructor through this service if desired. The student will need access to a Windows or Mac computer, and you'll need basic Internet service at home. The service uses the built-in microphone and headphone jack on the computer for audio. You can also purchase a pro audio upgrade package from JamKazam for $49.99 that includes an audio interface (a small box that connects to the computer via a USB cable), a microphone, a microphone cable, and a microphone stand. This is optional, but will deliver superior audio quality in lessons. + +\n\nThe music program directors are primarily concerned with giving our students the highest quality music education possible, so we encourage you to make whatever decision you feel is best for the student. That said, for students who take lessons through the JamKazam service, a portion of the lesson fees are distributed back into our music program booster fund, which helps to fund the program's expenses, and is a nice additional benefit. If you have more questions, you can send an email to support@jamkazam.com." + + return msg + + renderSchool: () -> firstNameErrors = context.JK.reactSingleFieldErrors('first_name', @state.inviteErrors) lastNameErrors = context.JK.reactSingleFieldErrors('last_name', @state.inviteErrors) emailErrors = context.JK.reactSingleFieldErrors('email', @state.inviteErrors) - firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true}) - lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true}) - emailClasses = classNames({email: true, error: emailErrors?, field: true}) + firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true}) + lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true}) + emailClasses = classNames({email: true, error: emailErrors?, field: true}) + sendInvitationClasses = classNames({'button-orange': true, disabled: this.state.sending}) if @state.teacher title = 'invite teacher' @@ -98,27 +155,39 @@ context = window
- + {firstNameErrors}
- + {lastNameErrors}
- + {emailErrors}
` + render: () -> + school = this.state.school + + if !school? + return `
no school
` + + if school.education && !@state.teacher + @renderEducation() + else + @renderSchool() + + }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index 67e5528f6..b504daa3b 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -32,10 +32,21 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ] onJamTrackStateChanged: (jamTrackState) -> + if window.unloaded + return + + if window.closed + return + @monitorControls(@state.controls, @state.mediaSummary, jamTrackState) @setState({jamTrackState: jamTrackState}) onMediaStateChanged: (changes) -> + if window.unloaded + return + if window.closed + return + if changes.playbackStateChanged if @state.controls? if changes.playbackState == 'play_start' @@ -51,6 +62,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) @setState({time: changes.time}) onInputsChanged: (sessionMixers) -> + if window.unloaded + return session = sessionMixers.session mixers = sessionMixers.mixers @@ -60,9 +73,16 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) metro = mixers.metro @monitorControls(@state.controls, mediaSummary, @state.jamTrackState) - @setState({mediaSummary: mediaSummary, metro: metro}) - @updateMetronomeDetails(metro, @state.initializedMetronomeControls) + state = {mediaSummary: mediaSummary, metro: metro} + try + @setState(state) + catch e + logger.error('MediaControls: unable to set state', state, e) + try + @updateMetronomeDetails(metro, @state.initializedMetronomeControls) + catch e + logger.error('MediaControls: unable to update metronome details', e) updateMetronomeDetails: (metro, initializedMetronomeControls) -> logger.debug("MediaControls: setting tempo/sound/cricket", metro) @@ -197,6 +217,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) @updateMetronomeDetails(metro, true) @setState({initializedMetronomeControls: true}) + shouldComponentUpdate:() -> + return !window.unloaded componentDidUpdate: (prevProps, prevState) -> @tryPrepareMetronome(@state.metro) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 0fd70d0d0..3f4dc1230 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -60,14 +60,33 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) session = sessionMixers.session mixers = sessionMixers.mixers + if @unloaded + #console.log("PopupMediaControls unloaded. ignore onMixersChnaged") + return + if window.closed + return @setState(@updateFromMixerHelper(mixers, session)) onMediaStateChanged: (changes) -> + if @unloaded + #console.log("PopupMediaControls unloaded. ignore onMixersChnaged") + return + + if window.closed + return + if changes.currentTimeChanged && @root? @setState({time: changes.time}) onJamTrackChanged: (changes) -> + if @unloaded + #console.log("PopupMediaControls unloaded. ignore onMixersChnaged") + return + + if window.closed + return + logger.debug("PopupMediaControls: jamtrack changed", changes) @setState({jamTrackState: changes}) @@ -446,7 +465,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) `
{header} - + {extraControls}
{helpButton} @@ -454,7 +473,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
` + windowUnloaded: () -> + logger.debug('PopupMediaControls: window uploaded') + @unloaded = true + window.unloaded = true + SessionActions.closeMedia(false) unless window.DontAutoCloseMedia toggleMyMixes: (e) -> @@ -691,6 +715,10 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged')) @resizeWindow() setTimeout(@resizeWindow, 1000) + shouldComponentUpdate: () -> + console.log("THIS UNLOADED", @unloaded) + return !@unloaded + resizeWindow: () => $container = $('#minimal-container') width = $container.width() diff --git a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee index 79c17b269..ccb3522a9 100644 --- a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee @@ -4,59 +4,106 @@ logger = context.JK.logger @SelectLocation = React.createClass({ - mixins: [Reflux.listenTo(@LocationStore,"onLocationsChanged")] + mixins: [Reflux.listenTo(@LocationStore, "onLocationsChanged")] propTypes: { onItemChanged: React.PropTypes.func.isRequired } - getInitialState:() -> - {selectedCountry: null, countries:{US: {name: 'United States', region: null}}} + getInitialState: () -> + + {selectedCountry: null, countries: LocationStore.countries || {US: {name: 'United States', regions: []}}} onLocationsChanged: (countries) -> + console.log("countires in ", countries) @setState({countries: countries}) onCountryChanged: (e) -> val = $(e.target).val() - @changed(val, null) - @setState({selectedCountry: val, selectedRegion: null }) + @changed(val, null, null) + @setState({selectedCountry: val, selectedRegion: null, selectedCity: null}) if val? LocationActions.selectCountry(val) onRegionChanged: (e) -> val = $(e.target).val() - @changed(@state.selectedCountry, val) - @setState({selectedRegion: val }) + @changed(this.currentCountry(), val, null) + @setState({selectedRegion: val, selectedCity: null}) - changed: (country, region) -> + if val? && this.props.showCity + LocationActions.selectRegion(this.currentCountry(), val) + + onCityChanged: (e) -> + val = $(e.target).val() + @changed(this.currentCountry(), this.currentRegion(), val) + @setState({selectedCity: val}) + + + changed: (country, region, city) -> if country == '' country = null if region == '' region = null - @props.onItemChanged(country, region) + if city == '' + city = null + + @props.onItemChanged(country, region, city) + + currentCity: () -> + this.state.selectedCity || this.props.selectedCity + + currentCountry: () -> + this.state.selectedCountry || this.props.selectedCountry || 'US' + + currentRegion: () -> + this.state.selectedRegion || this.props.selectedRegion + + defaultText: () -> + if this.props.defaultText? + this.props.defaultText + else + 'Any' render: () -> - countries = [``] + countries = [``] for countryId, countryInfo of @state.countries countries.push(``) - country = @state.countries[@state.selectedCountry] - regions = [``] + country = @state.countries[this.currentCountry()] + + regions = [``] + + cities = [``] if country? && country.regions for region in country.regions regions.push(``) + if this.currentRegion() == region.id && this.props.showCity + for city in region.cities + cities.push(``) + + if !this.props.hideCountry + countryJsx = `

Country:

+ +
` + disabled = regions.length == 1 + if this.props.showCity + cityJsx = `

City:

+ +
` + `
-

Country:

- + {countryJsx}

State/Region:

- + + {cityJsx}
` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index 0ee8dc023..1d91729d0 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -28,7 +28,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds SessionActions.downloadingJamTrack(false) @setState({downloadJamTrack: null}) - SessionActions.closeMedia(true) + SessionActions.closeMedia.trigger(true) #inputsChangedProcessed: (state) -> diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee index 71c25bdd6..aaefaf606 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -142,15 +142,20 @@ proficiencyDescriptionMap = { showSideBubble: () -> # :remaining_test_drives, :can_buy_test_drive? - if @state.user?['has_booked_test_drive_with_student'] + if @state.user?['same_school_with_student'] @showBuyNormalLessonBubble() + else if @user['jamclass_credits'] > 0 + @showUseRemainingJamClassCreditsBubble() else - if @user['remaining_test_drives'] > 0 - @showUseRemainingTestDrivesBubble() - else if @user['can_buy_test_drive?'] - @showBuyTestDriveBubble() - else + if @state.user?['has_booked_test_drive_with_student'] @showBuyNormalLessonBubble() + else + if @user['remaining_test_drives'] > 0 + @showUseRemainingTestDrivesBubble() + else if @user['can_buy_test_drive?'] + @showBuyTestDriveBubble() + else + @showBuyNormalLessonBubble() hideSideBubble: () -> if @screen.btOff @@ -159,6 +164,9 @@ proficiencyDescriptionMap = { showUseRemainingTestDrivesBubble: ( ) -> context.JK.HelpBubbleHelper.showUseRemainingTestDrives(@screen, @screen, @user, (() => @useRemainingTestDrives())) + showUseRemainingJamClassCreditsBubble: ( ) -> + context.JK.HelpBubbleHelper.showUseRemainingJamClassCreditsBubble(@screen, @screen, @user, (() => @useRemainingTestDrives())) + showBuyTestDriveBubble: () -> context.JK.HelpBubbleHelper.showBuyTestDrive(@screen, @screen, @user, (() => @buyTestDrive())) diff --git a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee index a668aa33d..accecbb73 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -158,7 +158,10 @@ ProfileActions = @ProfileActions rest.getTestDriveStatus({id: context.JK.currentUserId, teacher_id: user.id}) .done((response) => - if response.remaining_test_drives == 0 && response['can_buy_test_drive?'] + if response.jamclass_credits > 0 + logger.debug('TeacherSearchScreen: user has jamclass credits available') + window.location.href = '/client#/jamclass/book-lesson/test-drive_' + user.id + else if response.remaining_test_drives == 0 && response['can_buy_test_drive?'] logger.debug("TeacherSearchScreen: user offered test drive") #@app.layout.showDialog('try-test-drive', {d1: user.teacher.id}) window.location.href = '/client#/jamclass/test-drive-selection/' + user.id @@ -235,7 +238,7 @@ ProfileActions = @ProfileActions bookSingleBtn = null bookTestDriveBtn = null - if !school_on_school && (!@state.user? || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?']) + if !school_on_school && (!@state.user? || @state.user.jamclass_credits > 0 || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?']) bookTestDriveBtn = `BOOK TESTDRIVE LESSON` else bookSingleBtn = `BOOK LESSON` diff --git a/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee b/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee index 21b2953bd..c9f2f2a4c 100644 --- a/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee @@ -4,4 +4,5 @@ context = window load: {} selectCountry: {} + selectRegion: {} }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee new file mode 100644 index 000000000..2a6b6d004 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/RetailerActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@RetailerActions = Reflux.createActions({ + refresh: {}, + addInvitation: {}, + deleteInvitation: {} + updateRetailer: {} +}) + diff --git a/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee new file mode 100644 index 000000000..2aee0cc63 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamClassEducationLandingBottomPage.js.jsx.coffee @@ -0,0 +1,378 @@ +context = window +rest = context.JK.Rest() + +@JamClassEducationLandingBottomPage = React.createClass({ + + render: () -> + `
+
+

How JamClass by JamKazam Can Help Your Music School

+ +

Online music lessons offer major advantages to your students, private lesson teachers, and your school's + booster program.

+ +

+ Students can take lessons much more conveniently from home, while enjoying studio quality audio and while + retaining the ability to play live in sync with their instructor. Students can take lessons from the best + teacher vs. settling for someone who lives close by. Parents don't have to leave work early to drive students + to and from lessons during rush hour, while carting siblings along to lessons. A 30-minute lesson is just a + 30-minute lesson, not a 90-minute expedition. And students can record lessons to refer back to them later. +

+ +

+ Teachers can now provide lessons to students nearly anywhere, rather than being constrained to students who + live within a 30-minute drive. Teachers don't have to spend as much time driving to schools and to students' + homes as they do teaching, so they can travel less, teach more, and earn more. And teachers can provide + instruction to students from underserved schools that are located in areas that are more difficult to reach. +

+ + +

+ Even the booster program benefits, as JamKazam funnels a portion of lesson fees back into the music program + booster fund, helping to pay for trips, instrument repairs, and other music program expenses - all without + students selling things, and without additional time or effort expended by the music program director. +

+ +

Some teachers and students have historically tried using Skype to power online lessons, but have found that + the lesson experience is significantly diminished. Why? Because Skype and similar apps were built for voice + chat – not to deliver online music lessons. This is a major problem. Voice technology processes all audio as + if it were a spoken human voice, which makes music sound awful in online sessions – so bad that teachers can’t + assess the student’s tone and sometimes even the pitch of what they are playing. These apps also have very + high latency – a technical term that means that the student and teacher cannot play together, another + important requirement for productive lessons. Since Skype wasn’t built for music, it also lacks many other + basic features to support effective lessons, like a metronome, mixers, backing tracks, etc. +

+ +

+ At JamKazam, we’ve spent years designing, patenting, and building technology specifically to enable musicians + to play online live in sync with studio quality audio. We’ve built a wide variety of critical online music + performance features into this platform. And now we’ve built a lesson marketplace on top of this foundation, + and crafted a partner program specifically to meet the needs of secondary education music programs. The bottom + line is that your students, private lesson teachers, and your music program's booster fund can now all "win" + by adopting this amazing new Internet service. And you don't have to do it all at once. You can simply make + this available as an option to students and parents who decide this is a good fit for them and will help them. +

+ +

+ If this sounds interesting to you, read on to learn more about some of the top features of JamClass by + JamKazam. +

+ +
+

JamClass Kudos

+ +
+ + +

Scott Himel

+ +
+ Texas high school band director +
+
+
+ + +

Justin Pierce

+ +
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor +
+
+
+ + +

Dave Sebree

+ +
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician +
+
+
+ + +

Sara Nelson

+ +
+ Cellist for Austin Lyric Opera, frequently recorded with major artists +
+
+
+
+ +
+
+

+
1
+ Play Live In Sync From Different Locations +

+

+

+
+