merging origin

This commit is contained in:
Jonathan Kolyer 2016-10-08 19:06:38 +00:00
commit c23944607c
170 changed files with 7939 additions and 626 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -363,4 +363,9 @@ jamblasters_network.sql
immediate_recordings.sql
nullable_user_id_jamblaster.sql
rails4_migration.sql
non_free_jamtracks.sql
non_free_jamtracks.sql
retailers.sql
second_ed.sql
second_ed_v2.sql
retailers_v2.sql
retailer_interest.sql

View File

@ -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;

96
db/up/retailers.sql Normal file
View File

@ -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);

3
db/up/retailers_v2.sql Normal file
View File

@ -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);

4
db/up/second_ed.sql Normal file
View File

@ -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;

1
db/up/second_ed_v2.sql Normal file
View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN education_interest BOOLEAN NOT NULL DEFAULT FALSE;

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,36 @@
<% provide(:title, @subject) %>
<% if !@user.anonymous? %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p>
<% end %>
<p>
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.
</p>
<p>
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
<a href="https://jamkazam.desk.com/customer/en/portal/topics/985544-jamclass-online-music-lessons---for-secondary-education-music-program-directors/articles" style="color:#fc0">help
articles for music program directors</a>. 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.
</p>
<p>
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 <a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles">help guide for students</a> and our <a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles">help guide for teachers</a>.
</p>
<p>
Thanks again for connecting with us, and we look forward to speaking with you soon!
</p>
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -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

View File

@ -0,0 +1,16 @@
<% provide(:title, @subject) %>
Hello <%= @retailer_invitation.first_name %> -
<p><%= @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!</p>
<br/>
<p>
<a href="<%= @retailer_invitation.generate_signup_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">SIGN
UP NOW</a>
</p>
<br/>
<br/>
Best Regards,<br>
Team JamKazam

View File

@ -0,0 +1,18 @@
<% provide(:title, @subject) %>
Hello <%= @retailer_invitation.first_name %> -
<br/>
<p>
<%= @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!</p>
<br/>
<p>
<a href="<%= @retailer_invitation.generate_signup_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">SIGN
UP NOW</a>
</p>
<br/>
<br/>
Best Regards,<br>
Team JamKazam

View File

@ -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

View File

@ -0,0 +1,8 @@
<% provide(:title, @subject) %>
<p>Click the link of each teacher's profile at <%= @retailer.name %> to find the best fit for you:</p>
<ul>
<% @retailer.teachers.each do |teacher| %>
<li><a href="<%= teacher.user.teacher_profile_url%>" style="color:#fc0"><%= teacher.user.name %></a><br/><%= teacher.teaches %></li>
<% end %>
</ul>

View File

@ -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 %>

View File

@ -0,0 +1,14 @@
<% provide(:title, @subject) %>
<% if !@user.anonymous? %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p>
<% end %>
<p>
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.
</p>
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -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

View File

@ -0,0 +1,80 @@
<% provide(:title, @subject) %>
<% if !@user.anonymous? %>
<p>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
</p>
<% end %>
<% if @education %>
<p>
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.
</p>
<p>
To get ready to take JamClass lessons online, here are the things you'll want to do:
</p>
<p><b style="color: white">1. Set Up Your Gear</b><br/>
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 <a style="color:#fc0" href="mailto:support@jamkazam.com">support@jamkazam.com</a> or call us at <a href="tel:+18773768742" style="color:#fc0">1-877-376-8742</a>. 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.
</p>
<p><b style="color: white">2. Book Lessons</b><br/>
Once your gear is set up, you are ready to take lessons. Go to this web page: <a style="color:#fc0" href="<%= @user.school.teacher_list_url %>"><%= @user.school.teacher_list_url %></a>. 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.
</p>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
You can also review our <a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles">JamClass user guide for students</a>
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.
</p>
<p>
Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician!
</p>
<% else %>
<p>
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.
</p>
<p>
To get ready to take JamClass lessons online, here are the things you'll want to do:
</p>
<p><b style="color: white">1. Find a Teacher & Book Lessons</b><br/>
If you haven't done so already, <a href="https://www.jamkazam.com/client#/jamclass/searchOptions" style="color:#fc0">use this link to search our teachers</a>, 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:
<ul>
<li>With 4 different teachers for just $12.50 each</li>
<li>With 2 different teachers for just $14.99 each</li>
<li>Or with 1 teacher for just $14.99</li>
</ul>
<p>
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.
</p>
</p>
<p><b style="color: white">2. Set Up Your Gear</b><br/>
Please review our <a href="https://jamkazam.desk.com/customer/en/portal/topics/564807-gear-recommendations/articles" style="color:#fc0">help articles on gear recommendations</a>
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 <a href="https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles" style="color:#fc0">setup help articles</a> to download and install our free app and set it up with your audio gear and webcam. Please email us at <a href="mailto:support@jamkaazm.com" style="color:#fc0">support@jamkazam.com</a> or call us at <a href="tel:+18773768742" style="color:#fc0">1-877-376-8742</a> 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.
</p>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
Please review our <a href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles" style="color:#fc0">JamClass user guide for students</a> 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.
</p>
<p>
Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician!
</p>
<% end %>
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -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

View File

@ -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 @@
</p>
<p>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 havent booked your next TestDrive lesson,
<a href="<%= User.search_url %>" style="color:#fc0">click here</a> to search our teachers and get your next
lesson lined up today!</p>

View File

@ -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 todays lesson to help other students in the community find the best instructors.

View File

@ -6,21 +6,62 @@
</p>
<% end %>
<% if @education %>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
To get ready to take JamClass lessons online, here are the things you'll want to do:
</p>
<p>To take online lessons on JamKazam, you'll need the following things at home:</p>
<p><b style="color: white">1. Find a Teacher & Book Lessons</b><br/>
<ul>
<li>A Windows or Mac computer, with a built-in or external webcam for video, and a built-in microphone and headphone jack for audio</li>
<li>Home internet service</li>
</ul>
If you haven't done so already, <a href="https://www.jamkazam.com/client#/jamclass/searchOptions" style="color:#fc0">use this link to search our teachers</a>, 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:
<p>
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.
</p>
<p>
If you are curious to learn more about how everything works, you can also review our
<a href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles" style="color:#fc0">help
guide for students</a>. Thanks
again for connecting with us, and we look forward to speaking with you soon!
</p>
<% else %>
<p>
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.
</p>
<p>
To get ready to take JamClass lessons online, here are the things you'll want to do:
</p>
<p><b style="color: white">1. Find a Teacher & Book Lessons</b><br/>
If you haven't done so already,
<a href="https://www.jamkazam.com/client#/jamclass/searchOptions" style="color:#fc0">use this link to search our
teachers</a>, 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:
<ul>
<li>With 4 different teachers for just $12.50 each</li>
@ -28,25 +69,43 @@
<li>Or with 1 teacher for just $14.99</li>
</ul>
<p>
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.
</p>
</p>
</p>
<p><b style="color: white">2. Set Up Your Gear</b><br/>
Please review our <a href="https://jamkazam.desk.com/customer/en/portal/topics/564807-gear-recommendations/articles" style="color:#fc0">help articles on gear recommendations</a>
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 <a href="https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles" style="color:#fc0">setup help articles</a> to download and install our free app and set it up with your audio gear and webcam. Please email us at <a href="mailto:support@jamkaazm.com" style="color:#fc0">support@jamkazam.com</a> or call us at <a href="tel:+18773768742" style="color:#fc0">1-877-376-8742</a> 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.
</p>
<p><b style="color: white">2. Set Up Your Gear</b><br/>
Please review our
<a href="https://jamkazam.desk.com/customer/en/portal/topics/564807-gear-recommendations/articles" style="color:#fc0">help
articles on gear recommendations</a>
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
<a href="https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles" style="color:#fc0">setup
help articles</a> to download and install our free app and set it up with your audio gear and webcam. Please email
us at <a href="mailto:support@jamkaazm.com" style="color:#fc0">support@jamkazam.com</a> or call us at
<a href="tel:+18773768742" style="color:#fc0">1-877-376-8742</a> 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.
</p>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
Please review our <a href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles" style="color:#fc0">JamClass user guide for students</a> 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.
</p>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
Please review our
<a href="https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles" style="color:#fc0">JamClass
user guide for students</a> 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.
</p>
<p>
<p>
Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician!
</p>
Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn
and grow as a musician!
</p>
<% end %>
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -6,56 +6,113 @@
</p>
<% end %>
<p>
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.
</p>
<% if @education %>
<p>
To get ready to teach JamClass students online, here are the things you'll want to do:
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>To take online lessons on JamKazam, you'll need the following things at home:</p>
<ul>
<li>A Windows or Mac computer, with a built-in or external webcam for video, and a built-in microphone and headphone
jack for audio
</li>
<li>Home internet service</li>
</ul>
<p>
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.
</p>
<p>
If you are curious to learn more about how everything works, you can also review our <a href="https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles" style="color:#fc0">help guide for teachers</a>. Thanks
again for connecting with us, and we look forward to speaking with you soon!
</p>
<% else %>
<p>
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.
</p>
<p>
To get ready to teach JamClass students online, here are the things you'll want to do:
</p>
<p><b style="color: white">1. Set Up Your Teacher Profile</b><br/>
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.
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/2405835-creating-your-teacher-profile">Click
here for
instructions on filling out your teacher profile</a>.
</p>
<p><b style="color: white">1. Set Up Your Teacher Profile</b><br/>
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.
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/2405835-creating-your-teacher-profile">Click
here for
instructions on filling out your teacher profile</a>.
</p>
<p><b style="color: white">2. Set Up Your Gear</b><br/>
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements">Click
here for information on the gear requirements to effectively teach using the JamClass service</a>. When you have
everything you need,
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles">use
this set of help articles as a good step-by-step guide to set up your gear for use with the
JamKazam application</a>. 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.
</p>
<p><b style="color: white">2. Set Up Your Gear</b><br/>
<% if @education %>
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements">Click
here for information on the gear requirements to effectively teach using the JamClass service</a>. 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 %>
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements">Click
here for information on the gear requirements to effectively teach using the JamClass service</a>. When you have
everything you need,
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles">use
this set of help articles as a good step-by-step guide to set up your gear for use with the
JamKazam application</a>. 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 %>
</p>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles">Click
this link for a set of help articles specifically for teachers</a> 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
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles">use
this
link for a set of help articles that explain how to use the key features available to you in online sessions</a>
to
effectively teach students.
</p>
<p>
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 <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
We are happy to help you, and we look forward to helping you
reach and teach more students!
</p>
<% end %>
<p><b style="color: white">3. Learn About JamClass Features</b><br/>
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles">Click
this link for a set of help articles specifically for teachers</a> 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
<a style="color:#fc0" href="https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles">use
this
link for a set of help articles that explain how to use the key features available to you in online sessions</a> to
effectively teach students.
</p>
<p>
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 <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
We are happy to help you, and we look forward to helping you
reach and teach more students!
</p>
<p>Best Regards,<br/>
Team JamKazam</p>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -11,7 +11,7 @@ module JamRuby
@@log = Logging.logger[LessonSession]
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -294,6 +294,10 @@ def app_config
1
end
def jam_class_card_wait_period_year
1
end
def check_bounced_emails
false
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -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);

View File

@ -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);
})(window,jQuery);

View File

@ -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) {

View File

@ -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) {

View File

@ -9,6 +9,7 @@
var $screen = null;
function beforeShow(data) {
}
function afterShow(data) {

View File

@ -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);

View File

@ -298,7 +298,9 @@
}
}
monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500);
if (setTimeout) {
monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500);
}
}
function monitorRecordingPlayback() {

View File

@ -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

View File

@ -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'
`<div className="retailer-user">
<div className="avatar">
<img src={photo_url} />
</div>
<div className="usersname">
{user.name}
</div>
<div className="user-actions">
<a onClick={this.removeFromRetailer.bind(this, user.id, isTeacher)}>remove from retailer</a>
</div>
</div>`
renderInvitation: (invitation) ->
`<div key={invitation.id} className="retailer-invitation">
<table>
<tbody>
<td className="description">{invitation.first_name} {invitation.last_name}</td>
<td className="message">
<div className="detail-block">has not yet accepted invitation<br/>
<a className="resend" onClick={this.resendInvitation.bind(this, invitation.id)}>resend invitation</a>
<a className="delete" onClick={this.deleteInvitation.bind(this, invitation.id)}>delete</a>
</div>
</td>
</tbody>
</table>
</div>`
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 = `<p>No teachers</p>`
teachers
renderTeacherInvitations: () ->
invitations = []
if this.state.teacherInvitations? && this.state.teacherInvitations.length > 0
for invitation in this.state.teacherInvitations
invitations.push(@renderInvitation(invitation))
else
invitations = `<p>No pending invitations</p>`
invitations
mainContent: () ->
if !@state.user? || !@state.retailer?
`<div className="loading">Loading...</div>`
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 = `<a href={processUrl}>{processUrl}</a>`
`<div className="account-block info-block">
<div className={nameClasses}>
<label>Retailer Name:</label>
<input type="text" name="name" value={this.nameValue()} onChange={this.nameChanged}/>
{nameErrors}
</div>
<div className="field logo">
<label>Retailer Logo:</label>
<AvatarEditLink target={this.state.retailer} target_type="retailer"/>
</div>
<SelectLocation defaultText={'Not Specified'} showCity={true} hideCountry={true} onItemChanged={this.handleLocationChange} selectedCountry={'US'} selectedCity={this.state.retailer.city} selectedRegion={this.state.retailer.state} />
<div className="field password">
</div>
<div className="field password">
<div className="scooter">
<label>Retailer Username:</label>
<label >Administrator</label>
</div>
<div className="scooter">
<label>Retailer Password:</label>
<input type="password" defaultValue="" placeholder="leave blank for no change"/>
</div>
<div>
<label>Process Sale URL:</label>
{processSaleUrl} <span className="usage-hint">(enter Administrator/password to access this page)</span>
</div>
</div>
<h4>Payments</h4>
<div className="field stripe-connect">
<StripeConnect purpose='retailer' user={this.state.user}/>
</div>
<div className="actions">
<a className={classNames(cancelClasses)} onClick={this.onCancel}>CANCEL</a>
<a className={classNames(updateClasses)} onClick={this.onUpdate}>UPDATE</a>
</div>
</div>`
teachers: () ->
teachers = @renderTeachers()
teacherInvitations = @renderTeacherInvitations()
`<div className="members-block info-block">
<div className="column column-left">
<div>
<h3>teachers:</h3>
<a onClick={this.inviteTeacher} className="button-orange invite-dialog">INVITE TEACHER</a>
<br className="clearall" />
</div>
<div className="teacher-invites">
{teacherInvitations}
</div>
<div className="teachers">
{teachers}
</div>
</div>
</div>`
earnings: () ->
`<div className="earnings-block info-block">
<p>Coming soon</p>
</div>`
agreement: () ->
`<div className="agreement-block info-block">
<p>The agreement between your retailer and JamKazam is part of JamKazam's terms of service. You can find the
complete terms of service <a href="/corp/terms" target="_blank">here</a>. And you can find the section that is
most specific to the retailer terms <a href="/corp/terms" target="_blank">here</a>.</p>
</div>`
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 `<div key={i} className="profile-tile"><a className={classes}
onClick={this.selectionMade.bind(this, tile)}>{tile}</a></div>`
onCustomBack: (customBack, e) ->
e.preventDefault()
context.location = customBack
render: () ->
mainContent = @mainContent()
profileSelections = []
for tile, i in @TILES
profileSelections.push(@createTileLink(i, tile, profileSelections))
profileNav = `<div className="profile-nav">
{profileSelections}
</div>`
`<div className="content-body-scroller">
<div className="profile-header profile-head">
<div className="store-header">retailer:</div>
{profileNav}
<div className="clearall"></div>
</div>
<div className="profile-body">
<div className="profile-wrapper">
<div className="main-content">
{mainContent}
<br />
</div>
</div>
</div>
</div>`
})

View File

@ -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}"
`<div className="school-user">
<div className="avatar">
<img src={photo_url} />
<img src={photo_url}/>
</div>
<div className="usersname">
{user.name}
<span className="just-name">{user.name}</span>
<span className="just-email"><a href={mailto}>{user.email}</a></span>
</div>
<div className="user-actions">
<a onClick={this.removeFromSchool.bind(this, user.id, isTeacher)}>remove from school</a>
@ -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 = `<p>No teachers</p>`
@ -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 = `<div>
<h4>Management Preference</h4>
<div className="field scheduling_communication">
<div className="scheduling_communication school">
<input type="radio" name="scheduling_communication" readOnly={true} value="school"
checked={this.isSchoolManaged()}/><label>School owner will manage scheduling of student lessons
sourced
by JamKazam</label>
</div>
<div className="scheduling_communication teacher">
<input type="radio" name="scheduling_communication" readOnly={true} value="teacher"
checked={!this.isSchoolManaged()}/><label>Teacher will manage scheduling of lessons</label>
</div>
</div>
<div className={correspondenceEmailClasses}>
<label>Correspondence Email:</label>
<input type="text" name="correspondence_email" placeholder={ownerEmail} defaultValue={correspondenceEmail}
disabled={correspondenceDisabled}/>
<div className="hint">All emails relating to lesson scheduling will go to this email if school owner manages
scheduling.
</div>
{correspondenceEmailErrors}
</div>
</div>`
`<div className="account-block info-block">
<div className={nameClasses}>
@ -316,29 +359,7 @@ profileUtils = context.JK.ProfileUtils
<AvatarEditLink target={this.state.school} target_type="school"/>
</div>
<h4>Management Preference</h4>
<div className="field scheduling_communication">
<div className="scheduling_communication school">
<input type="radio" name="scheduling_communication" readOnly={true} value="school"
checked={this.isSchoolManaged()}/><label>School owner will manage scheduling of student lessons sourced
by JamKazam</label>
</div>
<div className="scheduling_communication teacher">
<input type="radio" name="scheduling_communication" readOnly={true} value="teacher"
checked={!this.isSchoolManaged()}/><label>Teacher will manage scheduling of lessons</label>
</div>
</div>
<div className={correspondenceEmailClasses}>
<label>Correspondence Email:</label>
<input type="text" name="correspondence_email" placeholder={ownerEmail} defaultValue={correspondenceEmail}
disabled={correspondenceDisabled}/>
<div className="hint">All emails relating to lesson scheduling will go to this email if school owner manages
scheduling.
</div>
{correspondenceEmailErrors}
</div>
{management}
<h4>Payments</h4>
@ -365,7 +386,7 @@ profileUtils = context.JK.ProfileUtils
<div>
<h3>teachers:</h3>
<a onClick={this.inviteTeacher} className="button-orange invite-dialog">INVITE TEACHER</a>
<br className="clearall" />
<br className="clearall"/>
</div>
<div className="teacher-invites">
{teacherInvitations}
@ -397,6 +418,73 @@ profileUtils = context.JK.ProfileUtils
<p>Coming soon</p>
</div>`
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 =
`<tr>
<td>{date}</td>
<td className="capitalize">{paymentMethod}</td>
<td>{description}</td>
<td className="capitalize">{status}</td>
<td className={classNames(amountClasses)}>{displayAmount}</td>
</tr>`
rows.push(row)
`<div>
<table className="payment-table">
<thead>
<tr>
<th>DATE</th>
<th>METHOD</th>
<th>DESCRIPTION</th>
<th>STATUS</th>
<th>AMOUNT</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<a className="btn-next-pager" href="/api/sales?page=1">Next</a>
<div className="end-of-payments-list end-of-list">No more payment history</div>
<div className="input-aligner">
<a className="back button-grey" onClick={this.onBack}>BACK</a>
</div>
<br className="clearall"/>
</div>`
agreement: () ->
`<div className="agreement-block info-block">
<p>The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the

View File

@ -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 = `<div className="hint">See how it will look to&nbsp;
<a href={testStudentUrl} target="_blank">students</a> and&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>`
else
previewArea = `<div className="hint">See how it will look to&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>`
`<div className="avatar-edit-link">
<img src={this.props.target.photo_url}></img>
<br/>
<a onClick={this.startUpdate}>change/update logo</a><br/>
<div className="hint">See how it will look to&nbsp;
<a href={testStudentUrl} target="_blank">students</a> and&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>
{previewArea}
</div>`
else
`<div className="avatar-edit-link">

View File

@ -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.<br/><br/>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 = `<h2>book testdrive lesson</h2>`
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 = `<div className="actions left">
<a className={cancelClasses} onClick={this.onCancel}>CANCEL</a>
@ -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 = `<div className="booking-info">
<p>You are booking a single 30-minute TestDrive session.</p>

View File

@ -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 = `<p>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. </p>`
else
title = 'invite student'
help = `<p>
Shouldn't be here...
</p>`
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>{title}</h1>
</div>
<div className="dialog-inner">
{help}
<div className={firstNameClasses}>
<label>First Name: </label>
<input type="text" defaultValue={this.firstName} name="first_name"/>
{firstNameErrors}
</div>
<div className={lastNameClasses}>
<label>Last Name: </label>
<input type="text" defaultValue={this.lastName} name="last_name"/>
{lastNameErrors}
</div>
<div className={emailClasses}>
<label>Email Name: </label>
<input type="text" defaultValue={this.email} name="email"/>
{emailErrors}
</div>
<div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doInvite} className={sendInvitationClasses}>SEND INVITATION</a>
</div>
</div>
</div>`
render: () ->
retailer = this.state.retailer
if !retailer?
return `<div>no retailer</div>`
@renderRetailer()
})

View File

@ -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: () ->
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>How to Invite Your Students</h1>
</div>
<div className="dialog-inner">
<p>
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.
</p>
<textarea readonly="true" value={this.educationCopyEmailText()}></textarea>
<div className="actions">
<a onClick={this.close} className="button-orange">DONE</a>
</div>
</div>
</div>`
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
<div className={firstNameClasses}>
<label>First Name: </label>
<input type="text" defaultValue={this.firstName} name="first_name" />
<input type="text" defaultValue={this.firstName} name="first_name"/>
{firstNameErrors}
</div>
<div className={lastNameClasses}>
<label>Last Name: </label>
<input type="text" defaultValue={this.lastName} name="last_name" />
<input type="text" defaultValue={this.lastName} name="last_name"/>
{lastNameErrors}
</div>
<div className={emailClasses}>
<label>Email Name: </label>
<input type="text" defaultValue={this.email} name="email" />
<input type="text" defaultValue={this.email} name="email"/>
{emailErrors}
</div>
<div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doInvite} className="button-orange">SEND INVITATION</a>
<a onClick={this.doInvite} className={sendInvitationClasses}>SEND INVITATION</a>
</div>
</div>
</div>`
render: () ->
school = this.state.school
if !school?
return `<div>no school</div>`
if school.education && !@state.teacher
@renderEducation()
else
@renderSchool()
})

View File

@ -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)

View File

@ -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'))
`<div className="media-controls-popup">
{header}
<MediaControls disabled={this.state.downloadingJamTrack || this.disableLoading}/>
<MediaControls unloaded={this.unloaded} disabled={this.state.downloadingJamTrack || this.disableLoading}/>
{extraControls}
<div className="actions">
{helpButton}
@ -454,7 +473,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
</div>
</div>`
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()

View File

@ -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 = [`<option key="" value="">Any</option>`]
countries = [`<option key="" value="">{this.defaultText()}</option>`]
for countryId, countryInfo of @state.countries
countries.push(`<option key={countryId} value={countryId}>{countryInfo.name}</option>`)
country = @state.countries[@state.selectedCountry]
regions = [`<option key="" value="">Any</option>`]
country = @state.countries[this.currentCountry()]
regions = [`<option key="" value="">{this.defaultText()}</option>`]
cities = [`<option key="" value="">{this.defaultText()}</option>`]
if country? && country.regions
for region in country.regions
regions.push(`<option key={region.id} value={region.id}>{region.name}</option>`)
if this.currentRegion() == region.id && this.props.showCity
for city in region.cities
cities.push(`<option key={city} value={city}>{city}</option>`)
if !this.props.hideCountry
countryJsx = `<div><h3>Country:</h3>
<select name="countries" onChange={this.onCountryChanged} value={this.currentCountry()}>{countries}</select>
</div>`
disabled = regions.length == 1
if this.props.showCity
cityJsx = `<div><h3>City:</h3>
<select name="cities" disabled={cities.length == 1} onChange={this.onCityChanged} value={this.currentCity()}>{cities}</select>
</div>`
`<div className="SelectLocation">
<h3>Country:</h3>
<select name="countries" onChange={this.onCountryChanged} value={this.state.selectedCountry}>{countries}</select>
{countryJsx}
<h3>State/Region:</h3>
<select name="regions" disabled={disabled} onChange={this.onRegionChanged} value={this.state.selectedRegion}>{regions}</select>
<select name="regions" disabled={disabled} onChange={this.onRegionChanged}
value={this.currentRegion()}>{regions}</select>
{cityJsx}
</div>`
})

View File

@ -28,7 +28,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds
SessionActions.downloadingJamTrack(false)
@setState({downloadJamTrack: null})
SessionActions.closeMedia(true)
SessionActions.closeMedia.trigger(true)
#inputsChangedProcessed: (state) ->

View File

@ -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()))

View File

@ -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 = `<a className="button-orange try-test-drive" onClick={this.bookTestDrive.bind(this, user)}>BOOK TESTDRIVE LESSON</a>`
else
bookSingleBtn = `<a className="button-orange try-normal" onClick={this.bookNormalLesson.bind(this, user)}>BOOK LESSON</a>`

View File

@ -4,4 +4,5 @@ context = window
load: {}
selectCountry: {}
selectRegion: {}
})

View File

@ -0,0 +1,9 @@
context = window
@RetailerActions = Reflux.createActions({
refresh: {},
addInvitation: {},
deleteInvitation: {}
updateRetailer: {}
})

View File

@ -0,0 +1,378 @@
context = window
rest = context.JK.Rest()
@JamClassEducationLandingBottomPage = React.createClass({
render: () ->
`<div className="top-container">
<div className="row awesome jam-class teachers">
<h2 className="awesome">How JamClass by JamKazam Can Help Your Music School</h2>
<p>Online music lessons offer major advantages to your students, private lesson teachers, and your school's
booster program.</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>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 cant
assess the students 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 wasnt built for music, it also lacks many other
basic features to support effective lessons, like a metronome, mixers, backing tracks, etc.
</p>
<p>
At JamKazam, weve spent years designing, patenting, and building technology specifically to enable musicians
to play online live in sync with studio quality audio. Weve built a wide variety of critical online music
performance features into this platform. And now weve 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.
</p>
<p>
If this sounds interesting to you, read on to learn more about some of the top features of JamClass by
JamKazam.
</p>
<div className="testimonials jam-class teachers">
<h3>JamClass Kudos</h3>
<div className="testimonial">
<img src="/assets/landing/Scott Himel - Speech Bubble.png" className="testimonial-speech-bubble"/>
<img src="/assets/landing/Scott Himel - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Scott Himel</strong></h4>
<div className="testiminal-background">
Texas high school band director
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Justin Pierce - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Justin Pierce - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Justin Pierce</strong></h4>
<div className="testiminal-background">
Masters degree in jazz studies, performer in multiple bands, saxophone instructor
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Dave Sebree - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Dave Sebree - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Dave Sebree</strong></h4>
<div className="testiminal-background">
Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Sara Nelson - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Sara Nelson - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Sara Nelson</strong></h4>
<div className="testiminal-background">
Cellist for Austin Lyric Opera, frequently recorded with major artists
</div>
</div>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">1</div>
Play Live In Sync From Different Locations
</h3>
<p>
<div className="video-wrapper right">
<div className="video-container">
<iframe src="//www.youtube.com/embed/I2reeNKtRjg" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
<p>Teacher and student need to be able to play together to enable effective lessons. As any teacher who has
attempted to teach using Skype will tell you, Skype doesn't let you play together. JamKazam's patented
technologies deliver on this requirement at an amazing level. Click the video above to watch 6 bands play
together from different locations to see our tech in action. And for an even more impressive feat, <a
href="https://www.youtube.com/watch?v=2Zk7-04IAx4" target="_blank">watch this video</a> with a band
playing together from Austin, Atlanta, Chicago, and Brooklyn using JamKazam tech.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">2</div>
Studio Quality Audio
</h3>
<p>
<div className="audio-wrapper">
<a href="https://www.jamkazam.com/recordings/94c5d5aa-2c61-440a-93a4-c661bf77d4a8" target="_blank">Sample
Session Audio #1</a>
<div className="sample-audio-text">Electric Guitars & Drum</div>
<a href="https://www.jamkazam.com/recordings/4916dbfe-0eeb-4bfb-b08a-4085dfecedcb" target="_blank">Sample
Session Audio #2</a>
<div className="sample-audio-text">Acoustic Guitar, Bass & Voice</div>
<a href="https://www.jamkazam.com/recordings/5875be7e-2cc3-4555-825c-046bd2f849e7" target="_blank">Sample
Session Audio #3</a>
<div className="sample-audio-text">Trumpet & Keys</div>
<p className="listening-note">These audio links will open a new tab in your browser. When done listening,
close the tab and return to this page.</p>
</div>
<p>Skype was built for voice - for people talking with each other. It uses something called a "voice codec".
This just means it processes all audio as a spoken human voice, and the result is that music, whether
instrumental or vocal, sounds very bad in Skype, as it has been processed through tech built for talking.
JamKazam delivers very high quality audio. You will be amazed at how good it sounds. It sounds like you're
sitting next to each other playing. This is also critical for a good lesson. Poor audio is hard to endure
in lessons.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">3</div>
Record Lessons & Student Performances
</h3>
<p>
<div className="video-wrapper left">
<div className="video-container">
<iframe src="//www.youtube.com/embed/KMIDnUlRiPs" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
<div className="cta-text">watch this sample video recording from a lesson</div>
</div>
<p>Many times a student thinks they've got it during a lesson, but they get home and realize "I don't got
it", and then they've wasted a week. In JamClass, you can easily record lessons to refer back to them
later. Students can also use our app to record their performances, upload them to YouTube, and share with
them the music program director.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">4</div>
Use JamTracks to Motivate Students
</h3>
<p>
<div className="video-wrapper right">
<div className="video-container">
<iframe src="//www.youtube.com/embed/07zJC7C2ICA" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
<p>Teachers can also use JamTracks to further motivate the student by letting them play with songs they
love. JamKazam offers a catalog of 3,700+ popular songs. Each song is is a complete multi-track recording,
with fully isolated tracks for each instrument and part of the music. So a student can listen to just the
part they're learning in isolation, turn around and mute that one part to play along with the rest of the
band, slow down playback for practice, record and share their performances, and more. It's really fun! And
a great way to keep your students motivated and engaged.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">5</div>
Broadcast Recitals
</h3>
<p>
<img src="/assets/landing/YouTube Logo.png" className="awesome-image right" width="264" height="117"/>
<p>When your music program adopts JamClass, you can easily live broadcast video and audio of student
recitals and even full band performances through YouTube - FREE. This enables other students, parents,
grandparents, and friends to "tune in" for these performances even if they cannot attend in person. This
can also be a great way to inspire and attract younger students in feeder schools without transporting
your entire band or orchestra for remote performances.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">6</div>
Apply VST & AU Audio Plug-In Effects
</h3>
<p>
<img src="/assets/landing/Top 10 Image - Number 6.png" className="awesome-image left" width="350"
height="240"/>
<p>The free JamKazam app lets you easily apply VST & AU plugin effects to your live performance in lessons.
For example, guitarists can apply popular amp sims like AmpliTube to get any kind of guitar tone without
pedal boards or amps, and vocalists can apply effects like reverb, pitch correction, etc.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class">
<div className="awesome-item">
<h3>
<div className="awesome-number">7</div>
Use MIDI Instruments
</h3>
<p>
<img src="/assets/landing/Top 10 Image - Number 7.png" className="awesome-image" width="320" height="257"/>
<p>The free JamKazam app also lets you use MIDI instruments in online lesson sessions. For example, keys
players can use MIDI keyboard controllers with VST & AU plugins to generate traditional piano sounds,
Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits
can use their favorite plugins to power their percussive audio.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome-thing jam-class multi-para">
<div className="awesome-item">
<h3>
<div className="awesome-number">8</div>
And So Much More...
</h3>
<p>
<p>There are many other features that are specifically useful for online lessons built into JamClass by
JamKazam, including a metronome feature, the ability for either teacher or student to open any audio file
and use it as a backing track for session acccompaniment, and too many more to list.</p>
<p>In addition to the lesson features, an awesome bonus is that once your students are set up to
play with your teachers in online lessons, they can also play completely FREE with anyone else
in the JamKazam community any time to use the skills theyre learning in lessons to play with
others, which again reinforces and motivates students to stay engaged, as its more fun to play
with others than alone. If you teach ensembles and rock bands, your students can practice in
groups between lessons without having to find rehearsal space, pack gear, and travel. Plus
there are thousands of online sessions played every month on the JamKazam service, including
open jam sessions set up by our user community, and students can hop into these sessions,
create their own improptu sessions, etc. It's a vibrant and welcoming community of fellow
musicians.</p>
<div className="clearall"/>
</p>
</div>
</div>
<div className="row awesome jam-class">
<h2 className="awesome">How Does My Music Program Start Using JamClass By JamKazam?</h2>
<p>
One of the great things about this program is that it's extremely easy to get started, and very flexible to
your preferences. To get started the music program director simply enters his or her email address and a
password at the top of this page to express an interest in signing up. This is not a commitment, just an
expression of interest. Someone from JamKazam will follow up with you to answer all your questions. And then
if you decide this is a good fit for your music program, it takes only an hour of your time (or less) to set
up the program, and we're happy to help you do it step by step. JamKazam does everything else, so this won't
be a drain on your time or energy, and you can stay completely focused on your students and your program.
Additionally, this is not an all-or-nothing service. It's very flexible. You can offer this service to your
music program's parents and students as a helpful option, and anyone who wants to use it can use it, and no
one else needs to use it.
</p>
</div>
<div className="row awesome jam-class">
<h2 className="awesome">What Equipment Does The Student Need?</h2>
<p>
A student's family needs to have either a Windows or Mac computer at home that the student can use, and basic
Internet service. Nothing fancy at all. The JamKazam can use the built-in microphone and built-in webcam on
these computers to capture audio and video, and the student can plug a pair of headphones or earbuds into the
computer to hear the high-quality audio.
</p>
<p>
For parents and students who want the best possible audio quality, JamKazam offers a truly amazing package
deal (below our cost), so that a parent/student may purchase a pro quality audio gear package for just $49.99.
With this gear, the student can enjoy studio quality audio that is superior to what a built-in microphone can
capture. This package includes an audio interface (a small box that plugs into the computer using a USB
cable), a microphone, a mic cable, and a mic stand.
</p>
<p>
JamKazam provides 1:1 support to both teachers and students to help them get everything set up and working
properly, and our staff get into an online session to verify that everything is working great, and to show the
student around the key features they can use in online sessions.
</p>
</div>
<div className="row awesome jam-class">
<h2 className="awesome">How Do the Business Aspects of JamClass Work?</h2>
<p>JamKazam handles all student billing for lessons, so you and the instructors don't have to worry about this.
Parents pay online using a highly secure credit card processing system powered by Stripe, one of the largest
and most secure Internet commerce processing platforms in the world.</p>
<p>When a parent/student pays for a lesson, or for a month of lessons, JamKazam immediately distributes 75%
(less 3% for Stripe transaction processing fees) of the lesson fees to the lesson instructor. The instructor
gives up some income on each lesson, but our service enables instructors to travel less, reach more students,
and spend more time teaching (and earning). So instructors actually earn more and win with this program
too.</p>
<p>And finally, JamKazam distributes 6.25% of the lesson fees into the music program's booster fund to help your
program pay for trips, instrument repairs, or whatever other expenses your program needs to fund. This is
processed as a direct deposit, so even this takes no time or effort from you to manage.</p>
</div>
<div id="what-now" className="row awesome jam-class">
<h2 className="awesome">What Now?</h2>
<p>
If you're ready to sign up your school, or you think this might be good for your school but are not sure yet,
scroll back up to the top of this page, and enter your email address and a password. Once you've done this,
we'll reach out to you to answer any and all questions you have. If you find you want to move forward, well
work with you directly to help you get your school ready to go. We're excited that you are considering this
JamKazam service, and we look forward to hearing from you!
</p>
</div>
</div>`
})

View File

@ -0,0 +1,178 @@
context = window
rest = context.JK.Rest()
@JamClassEducationLandingPage = React.createClass({
render: () ->
loggedIn = context.JK.currentUserId?
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "SET UP SCHOOL"
else
ctaButtonText = "SIGN UP"
if loggedIn
register = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="jamtrack-signup-form">
<label>Email: </label><input type="text" name="email"/>
<label>Password: </label><input type="password" name="password"/>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
</div>`
`<div className="top-container">
<div className="full-row name-and-artist">
<div>
<div className="jam-class-ed-video">
<iframe src="//www.youtube.com/embed/wdMN1fQyD9k" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
<h1 className="jam-track-name">MAKE LESSONS MORE CONVENIENT</h1>
<h2 className="original-artist">And give your booster fund a boost!</h2>
<div className="clearall"/>
</div>
<JamClassPhone customClass="school"/>
<div className="preview-and-action-box jamclass school">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass"/>
<div className="preview-jamtrack-header">
Sign Up Your School
</div>
<div className={classNames({'preview-area': true, 'jam-class': true})}>
<p>Sign up to let us know youre interested in partnering, and well follow up to answer your
questions.</p>
<p>If this is a good fit for your school, well give you all the 1:1 help you need to get your school
and staff up and running.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy
policy</a></p>
{register}
<p>It takes less than 1 hour of your time to set up this program for your school! We do everything else.</p>
</div>
</div>
</div>
<div className="row summary-text">
<p className="top-summary">
JamKazam has developed remarkable new technology that lets musicians play together live in sync with studio
quality audio from different locations over the Internet. Now JamKazam has launched an online music lesson
marketplace, and weve set up a program specifically to partner with secondary education music programs to
make lessons more convenient for students and parents, to help instructors teach more, and to simultaneously
contribute to your music program's booster fund.
</p>
</div>
</div>`
getInitialState: () ->
{loginErrors: null, processing: false}
privacyPolicy: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/privacy')
termsClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/terms')
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
@markTeacher()
else
@createUser()
@setState({processing: true})
markTeacher: () ->
rest.updateUser({school_interest: true,education_interest: true})
.done((response) =>
this.setState({done: true})
context.location = '/client#/account/school'
)
.fail((jqXHR) =>
this.setState({processing: false})
context.JK.app.notifyServerError(jqXHR, "Unable to Mark As Interested in School")
)
createUser: () ->
$form = $('.jamtrack-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
school_interest: true,
education_interest: true
})
.done((response) =>
if response.autologin
context.location = '/client#/account/school'
else
context.location = '/client#/home'
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
@setState({processing: true})
})

View File

@ -0,0 +1,102 @@
context = window
rest = context.JK.Rest()
@JamClassRetailerLandingBottomPage = React.createClass({
render: () ->
`<div className="top-container">
<div className="row awesome jam-class teachers">
<h2 className="awesome">How Our Retail Partner Program Can Help Your Store</h2>
<p>By simply adding our free countertop display of music lesson and JamTracks gift cards to your store, you can
instantly be enabled to offer an amazing deal on music lessons to every customer who buys an instrument from
your store. Students can connect with amazing teachers anywhere in the country, avoid the time and hassle of
travel to/from lessons, and even record lessons to avoid forgetting what theyve learned. Even if your store
offers lessons through in-store teachers, often your customers may live too far away or may not want to deal
with travel to get back to your store for lessons, and this is a great service you can offer, while earning
about $100 per year per student for students who stick with their lessons, in addition to 30% on the initial
gift card sale.</p>
<p>And for more advanced musicians who frequently wander into your store just to look around because they have
the bug, JamTracks are a terrific product you can sell to both beginner and advanced musicians. JamTracks
are full multitrack recordings of more than 4,000 popular songs. Your customers can solo a part they want to
play to hear all its nuances, mute that part out to play with the rest of the band, slow it down to practice,
record themselves playing along and share it with their friends on YouTube, and more. Youll earn 30% margins
on JamTracks gift cards as well.</p>
<p>Watch the videos below that explain and show our JamClass online music lesson service and our JamTracks
products in more detail.</p>
<div className="testimonials jam-class retailer">
<h3>JamClass Kudos</h3>
<div className="testimonial">
<img src="/assets/landing/Julie Bonk - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Julie Bonk - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Julie Bonk</strong></h4>
<div className="testiminal-background">
Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Carl Brown - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Carl Brown - Avatar.png" className="testimonial-avatar"/>
<a rel="external" href="https://www.youtube.com/channel/UCvnfBBzEizi1T5unOXNCxdQ"><img
src="/assets/landing/Carl Brown - YouTube.png" className="testimonial-youtube"/></a>
<h4><strong>Carl Brown</strong> of GuitarLessions365</h4>
</div>
<div className="testimonial">
<img src="/assets/landing/Justin Pierce - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Justin Pierce - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Justin Pierce</strong></h4>
<div className="testiminal-background">
Masters degree in jazz studies, performer in multiple bands, saxophone instructor
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Dave Sebree - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Dave Sebree - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Dave Sebree</strong></h4>
<div className="testiminal-background">
Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Sara Nelson - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Sara Nelson - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Sara Nelson</strong></h4>
<div className="testiminal-background">
Cellist for Austin Lyric Opera, frequently recorded with major artists
</div>
</div>
</div>
</div>
<div className="video-section">
<div className="video-wrapper">
<div className="video-container">
<iframe src="https://www.youtube.com/v/Y9m16G_86oU" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>
<div className="video-section second">
<div className="video-wrapper">
<div className="video-container">
<iframe src="https://www.youtube.com/v/-rHfJggbgqk" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>
<br className="clearall"/>
</div>`
})

View File

@ -0,0 +1,171 @@
context = window
rest = context.JK.Rest()
@JamClassRetailerLandingPage = React.createClass({
render: () ->
loggedIn = context.JK.currentUserId?
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "SIGN UP"
else
ctaButtonText = "SIGN UP"
if loggedIn
register = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="jamtrack-signup-form">
<label>Email: </label><input type="text" name="email"/>
<label>Password: </label><input type="password" name="password"/>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
</div>`
`<div className="top-container">
<div className="full-row name-and-artist">
<div>
<img className="jam-class-teacher" width="375" height="215" src="/assets/landing/jam_class.png"
alt="teacher instructing a jam class"/>
<h1 className="jam-track-name">MANAGE A MUSIC INSTRUMENT STORE?</h1>
<h2 className="original-artist">Increase revenues without more inventory or space</h2>
<div className="clearall"/>
</div>
<JamClassPhone customClass="retailer"/>
<div className="preview-and-action-box jamclass retailer">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass"/>
<div className="preview-jamtrack-header">
Sign Up Your Store
</div>
<div className={classNames({'preview-area': true, 'jam-class': true})}>
<p>Sign up to let us know youre interested in partnering, and well follow up to answer your questions.</p>
<p>If this is a good fit for your store, well help set up your JamKazam account and ship you a countertop
display with POSA cards.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy
policy</a></p>
{register}
<p>Learn how we can help you increase revenues without additional inventory or floor space.</p>
</div>
</div>
</div>
<div className="row summary-text">
<p className="top-summary">
Founded by a team that has built and sold companies to Google, eBay, GameStop and more, JamKazam has built
technology that lets musicians play together live in sync with studio quality audio over the Internet. Now
weve launched both an online music lesson marketplace, as well as a JamTracks marketplace that lets musicians
play along with their favorite bands and songs in compelling new ways. And weve created a simple, profitable
POSA card (point of sale activated gift card) program that retailers can use to sell these products without
any inventory investment or floor space.
</p>
</div>
</div>`
getInitialState: () ->
{loginErrors: null, processing: false}
privacyPolicy: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/privacy')
termsClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/terms')
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
@markTeacher()
else
@createUser()
@setState({processing: true})
markTeacher: () ->
rest.updateUser({retailer_interest: true})
.done((response) =>
this.setState({done: true})
context.location = '/client#/home'
)
.fail((jqXHR) =>
this.setState({processing: false})
context.JK.app.notifyServerError(jqXHR, "Unable to Mark As Interested in School")
)
createUser: () ->
$form = $('.jamtrack-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
retailer_interest: true
})
.done((response) =>
context.location = '/client#/home'
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
@setState({processing: true})
})

View File

@ -4,8 +4,6 @@ rest = context.JK.Rest()
@JamClassSchoolLandingPage = React.createClass({
render: () ->
loggedIn = context.JK.currentUserId?
if this.state.done
@ -19,26 +17,31 @@ rest = context.JK.Rest()
ctaButtonText = "SIGN UP"
if loggedIn
register = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>`
register = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors, {email: 'Email', password: 'Password', 'terms_of_service' : 'The terms of service'})
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="jamtrack-signup-form">
<label>Email: </label><input type="text" name="email" />
<label>Password: </label><input type="password" name="password" />
<label>Email: </label><input type="text" name="email"/>
<label>Password: </label><input type="password" name="password"/>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms" /><label className="terms-help">I have read and agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
</div>`
@ -46,14 +49,20 @@ rest = context.JK.Rest()
`<div className="top-container">
<div className="full-row name-and-artist">
<div>
<img className="jam-class-teacher" width="375" height="215" src="/assets/landing/jam_class.png" alt="teacher instructing a jam class"/>
<img className="jam-class-teacher" width="375" height="215" src="/assets/landing/jam_class.png"
alt="teacher instructing a jam class"/>
<h1 className="jam-track-name">GROW YOUR SCHOOLS REACH & INCOME</h1>
<h2 className="original-artist">Do you own/operate a music school?</h2>
<div className="clearall"/>
</div>
<JamClassPhone/>
<div className="preview-and-action-box jamclass">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass" />
<JamClassPhone customClass="school"/>
<div className="preview-and-action-box jamclass school">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass"/>
<div className="preview-jamtrack-header">
Sign Up Your School
</div>
@ -61,36 +70,31 @@ rest = context.JK.Rest()
<p>Sign up to let us know youre interested in partnering, and well follow up to answer your
questions.</p>
<p>If this is a good fit for your school, well give you all the 1:1 help you need to get your school
and staff up and running.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy policy</a></p>
{register}
<p>Learn how we can help you greatly extend your reach to new markets while increasing your
revenues.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy
policy</a></p>
{register}
<p>Learn how our program helps you, students, teachers, and your booster fund all win!</p>
</div>
</div>
</div>
<div className="row summary-text">
<p className="top-summary">
Founded by a team that has built and sold companies to Google, eBay, GameStop and more,
JamKazam has developed incredibly unique technology that lets musicians play together live in
sync with studio quality audio from different locations over the Internet. Now JamKazam has
launched an online music lesson marketplace, and weve set up a program specifically to
partner with music schools to help you attract and engage students across the country,
extending your schools reach and generating more income.
Founded by a team that has built and sold companies to Google, eBay, GameStop and more, JamKazam has developed
incredibly unique technology that lets musicians play together live in sync with studio quality audio from
different locations over the Internet. Now JamKazam has launched an online music lesson marketplace, and weve
set up a program specifically to partner with music schools to help you attract and engage students across the
country, extending your schools reach and generating more income.
</p>
</div>
</div>`
getInitialState: () ->
{loginErrors: null, processing:false}
{loginErrors: null, processing: false}
privacyPolicy: (e) ->
e.preventDefault()
@ -102,12 +106,12 @@ rest = context.JK.Rest()
context.JK.popExternalLink('/corp/terms')
componentDidMount:() ->
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
@ -122,7 +126,7 @@ rest = context.JK.Rest()
else
@createUser()
@setState({processing:true})
@setState({processing: true})
markTeacher: () ->
@ -142,11 +146,18 @@ rest = context.JK.Rest()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({email: email, password: password, first_name: null, last_name: null, terms:terms, school_interest: true})
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
school_interest: true
})
.done((response) =>
context.location = '/client#/home'
).fail((jqXHR) =>
@setState({processing:false})
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@ -158,5 +169,5 @@ rest = context.JK.Rest()
)
@setState({processing:true})
@setState({processing: true})
})

View File

@ -0,0 +1,200 @@
context = window
rest = context.JK.Rest()
@PosaActivationPage = React.createClass({
render: () ->
if this.props.retailer.large_photo_url?
logoImg = `<img src={this.props.retailer.large_photo_url}/>`
logo = `<div className="retailer-logo">
{logoImg}
<div className="retailer-name">
{this.props.retailer.name}
</div>
<br className="clearall"/>
</div>`
ctaButtonText = 'ACTIVATE'
if @state.processing
ctaButtonText = 'HOLD ON'
console.log("this.props.retailer", this.props.retailer, this.props.has_teachers)
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
if this.state.emailErrors?
for errorKey, value of this.state.emailErrors
break
success = `<div className="success-message">
{this.state.success}
</div>`
emailSuccess = `<div className="success-message">
{this.state.emailSuccess}
</div>`
posaErrors = context.JK.getFullFirstError(key, this.state.loginErrors,
{code: 'This code', activated_at: 'This code', claimed_at: 'Claimed', user: 'User', retailer: 'Retailer'})
emailErrors = context.JK.getFullFirstError(errorKey, this.state.emailErrors,
{email: 'Email address', retailer: 'Retailer'})
register = `<div className="register-area jam-class">
<form className="retailer-signup-form" onSubmit={this.submit}>
<div className="field">
<label>POSA Card Code: </label><input type="text" name="code"/>
</div>
<a
className={classNames({'activate-btn': true, 'button-orange':true, 'cta-button' : true, 'processing': this.state.processing})}
onClick={this.activateClick}>{'ACTIVATE'}</a>
</form>
{success}
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{posaErrors}
</div>
</div>`
sendEmail = `<div className="send-email jam-class">
<form className="retailer-email-form" onSubmit={this.submitEmail}>
<div className="field">
<label>Customer Email: </label><input type="text" name="email"/>
</div>
<a
className={classNames({'activate-btn': true, 'button-orange':true, 'cta-button' : true, 'processing': this.state.processing})}
onClick={this.submitEmail}>{'SEND LINKS'}</a>
</form>
{emailSuccess}
<div className={classNames({'errors': true, 'active': this.state.emailErrors})}>
{emailErrors}
</div>
</div>`
leftColumnClasses = classNames({column: true, has_teachers: this.props.has_teachers})
rightColumnClasses = classNames({column: true, has_teachers: this.props.has_teachers})
`<div className="container">
<div className={leftColumnClasses}>
<div className="header-area">
<div className="header-content">
{logo}
<div className="headers">
<h1>ACTIVATE JAMKAZAM POSA CARD</h1>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
Please enter the 10-digit code from the back of the POSA card you have sold, and then click the Activate
Card button:
</p>
</div>
{register}
</div>
<div className={rightColumnClasses}>
<div className="header-area">
<div className="header-content">
<div className="headers">
<h1>SEND TEACHER LINKS TO STUDENT</h1>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
If you want to send links to your stores teachers to thfcustomer, enter their email address below, and click the Send Links button.
</p>
</div>
{sendEmail}
</div>
<div className="clearall"/>
</div>`
submit: (e) ->
@activateClick(e)
getInitialState: () ->
{loginErrors: null, processing: false, emailErrors: null}
componentDidMount: () ->
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
activateClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({processing: true, success: null, emailSuccess:null, loginErrors: null})
@activateCode()
submitEmail: (e) ->
e.preventDefault()
return if @state.processing
$form = $('.retailer-email-form')
email = $form.find('input[name="email"]').val()
emailErrors = null
if(!email)
emailErrors = {"email": ['must be specified']}
processing = false
else
processing = true
rest.sendRetailerCustomerEmail({retailer: this.props.retailer.id, email: email})
.done((response) =>
@setState({processing: false, emailSuccess: "List of teachers sent to #{email}."})
$form.find('input[name="email"]').val('')
)
.fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 404
@setState({emailErrors: {"retailer": ['is not valid']}})
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({emailErrors: response.errors})
else
context.JK.app.notify({title: 'Unable to Send Email', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Send Email")
)
@setState({processing: processing, success: null, emailSuccess: null, emailErrors: emailErrors})
activateCode: () ->
$form = $('.retailer-signup-form')
code = $form.find('input[name="code"]').val()
rest.posaActivate({
code: code,
slug: @props.retailer.slug
})
.done((response) =>
@setState({processing: false, success: 'Card successfully activated. Please give card to customer. Thank you!'})
).fail((jqXHR) =>
@setState({processing: false})
console.log("jqXHR.status", jqXHR.status)
if jqXHR.status == 404
@setState({loginErrors: {"code": ['is invalid. Please try entering the code again. If it still will not work, try a different card, and please email us at support@jamkazam.com so we can resolve the problem with this card.']}})
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unable to Activate POSA Card', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Activate POSA Card")
)
})

View File

@ -2,6 +2,7 @@ context = window
rest = context.JK.Rest()
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
badCode = 'This is not a valid code. Please carefully re-enter the code and try again. If it still does not work, please email us at support@jamkazam.com to report this problem.'
@RedeemGiftCardPage = React.createClass({
render: () ->
@ -13,14 +14,32 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
errorText = context.JK.getFullFirstError(key, @state.formErrors, {email: 'Email', password: 'Password', gift_card: 'Gift Card Code', 'terms_of_service' : 'The terms of service'})
if errorText? && errorText.indexOf('does not exist') > -1
errorText = 'This is not a valid code. Please carefully re-enter the code and try again. If it still does not work, please email us at support@jamkazam.com to report this problem.'
if errorText? && errorText.indexOf('must already be set') > -1
errorText = 'This card has not been activated by a retailer and cannot currently be used. If you purchased this card from a store, please return to the store and have the store activate the card. Only the store where you purchased this card can activate it.'
if errorText? && errorText.indexOf('already claimed') > -1
errorText = 'This card has already been claimed. If you believe this is in error, please email us at support@jamkazam.com to report this problem.'
buttonClassnames = classNames({'redeem-giftcard': true, 'button-orange': true, disabled: @state.processing || @state.done })
if @state.done
button =
`<div key="done" className="done-action">
<div>You have {this.state.gifted_jamtracks} free JamTracks on your account!</div>
<div>You can now <a className="go-browse" href="/client#/jamtrack">browse our collection</a> and redeem them.</div>
</div>`
if this.state.gifted_jamtracks
button =
`<div key="done" className="done-action">
<div>You now have {this.state.gifted_jamtracks} JamTracks credits on your account!</div>
<div><a className="go-browse" href="/client#/jamtrack">go to JamTracks home page</a></div>
</div>`
else
button =
`<div key="done" className="done-action">
<div>You now have {this.state.gifted_jamclass}, 30-minute JamClass credits on your account!</div>
<div><a className="go-browse" href="/client#/jamclass/searchOptions">go to JamClass teacher search page</a></div>
</div>`
else
button = `<button key="button" className={buttonClassnames} onClick={this.action}>REDEEM GIFT CARD</button>`
@ -35,7 +54,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
<label>Gift Card Code:</label><input type="text" name="code"/>
{action}
</form>`
instruments = `<p className="instructions">Enter the code from the back of your gift card to associate it with your account.</p>`
instruments = `<p className="instructions">Enter the 10-digit code from the back of your gift card and click the Redeem button below.</p>`
else
form =
`<form onSubmit={this.submit}>
@ -47,7 +66,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
<div className="clearall"/>
{action}
</form>`
instruments = `<p className="instructions">Enter the code from the back of your gift card to associate it with your new JamKazam account.</p>`
instruments = `<p className="instructions">Enter the 10-digit code from the back of your gift card and click the Redeem button below.</p>`
classes = classNames({'redeem-container': true, 'not-logged-in': !context.JK.currentUserId?, 'logged-in': context.JK.currentUserId? })
@ -105,7 +124,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest.redeemGiftCard({gift_card: code})
.done((response) =>
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks})
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass})
).fail((jqXHR) =>
@setState({processing:false})
@ -142,7 +161,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest.signup({email: email, password: password, gift_card: code, terms: terms})
.done((response) =>
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks})
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass})
).fail((jqXHR) =>
@setState({processing:false})

View File

@ -0,0 +1,147 @@
context = window
rest = context.JK.Rest()
@RetailerTeacherLandingPage = React.createClass({
render: () ->
if this.props.retailer.large_photo_url?
logo = `<div className="retailer-logo">
<img src={this.props.retailer.large_photo_url}/>
</div>`
loggedIn = context.JK.currentUserId? && !this.props.preview
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "ALREADY A JAMKAZAM USER"
else
ctaButtonText = "SIGN UP"
if loggedIn
register =
`<div className="register-area jam-class">
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</div>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="retailer-signup-form">
<div className="field">
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
</div>
<div className="field">
<label>Password: </label><input type="password" name="password"/>
</div>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" target="_blank">terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
<p className="privacy-policy">
We will not share your email.<br/>See our <a href="/corp/privacy" target="_blank">privacy policy</a>.
</p>
</div>`
`<div className="container">
<div className="header-area">
<div className="header-content">
{logo}
<div className="headers">
<h1>REGISTER AS A TEACHER</h1>
<h2>with {this.props.retailer.name}</h2>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
Please register here if you are currently a teacher with {this.props.retailer.name}, and if you plan to teach
online music lessons for students of {this.props.retailer.name} using the JamKazam service. When you have registered, we
will
email you instructions to set up your online teacher profile, and we'll schedule a brief online training session to make sure
you are comfortable using the service and ready to go with students in online lessons.
</p>
</div>
{register}
</div>`
getInitialState: () ->
{loginErrors: null, processing: false}
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{context.JK.currentUserId}"
else
@createUser()
@setState({processing:true})
createUser: () ->
$form = $('.retailer-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
teacher: true,
retailer_invitation_code: this.props.invitation_code,
retailer_id: this.props.retailer.id
})
.done((response) =>
@setState({done: true})
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{response.id}"
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
})

View File

@ -16,7 +16,7 @@ rest = context.JK.Rest()
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "GO TO JAMKAZAM"
ctaButtonText = "ALREADY A JAMKAZAM USER"
else
ctaButtonText = "SIGN UP"
@ -40,7 +40,7 @@ rest = context.JK.Rest()
</div>
<form className="school-signup-form">
<div className="field">
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
</div>
<div className="field">
<label>Password: </label><input type="password" name="password"/>
@ -54,11 +54,27 @@ rest = context.JK.Rest()
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
<p className="privacy-policy">
<div className="privacy-policy">
We will not share your email.<br/>See our <a href="/corp/privacy" target="_blank">privacy policy</a>.
</p>
</div>
</div>`
if !this.props.school.education
explain = `<p>
Please register here if you are currently a student with {this.props.school.name}, and if you plan to take
online music lessons from {this.props.school.name} using the JamKazam service. When you have registered, we
will
email you instructions to set up your profile, and we'll schedule a brief online training session to make sure
you are comfortable using the service and ready to go for your first online lesson.
</p>`
else
explain = `<p>
Please register here if you are currently a student with {this.props.school.name}, and if you are interested in
taking online music lessons using JamKazam. When you have registered, someone from JamKazam will contact you to
answer any questions you have about our online lesson service, and to help you decide if this is service is a
good option for you. If it is, we'll help you get set up and ready to go, and will get into an online session to
make sure everything is working properly.
</p>`
`<div className="container">
<div className="header-area">
@ -75,15 +91,8 @@ rest = context.JK.Rest()
</div>
<div className="explain">
<p>
Please register here if you are currently a student with {this.props.school.name}, and if you plan to take
online music lessons from {this.props.school.name} using the JamKazam service. When you have registered, we
will
email you instructions to set up your profile, and we'll schedule a brief online training session to make sure
you are comfortable using the service and ready to go for your first online lesson.
</p>
{explain}
</div>
{register}
</div>`
@ -106,12 +115,12 @@ rest = context.JK.Rest()
loggedIn = context.JK.currentUserId?
if loggedIn
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{context.JK.currentUserId}"
window.location.href = "/client#/home"
#window.location.href = "/client#/profile/#{context.JK.currentUserId}"
else
@createUser()
@setState({processing:true})
@setState({processing: true})
createUser: () ->
$form = $('.school-signup-form')
@ -131,8 +140,15 @@ rest = context.JK.Rest()
})
.done((response) =>
@setState({done: true})
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{response.id}"
redirectTo = $.QueryString['redirect-to'];
if redirectTo
logger.debug("redirectTo:" + redirectTo);
window.location.href = redirectTo;
else
logger.debug("default post-login path");
window.location.href = "/client#/home"
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422

View File

@ -16,7 +16,7 @@ rest = context.JK.Rest()
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "GO TO JAMKAZAM"
ctaButtonText = "ALREADY A JAMKAZAM USER"
else
ctaButtonText = "SIGN UP"
@ -40,7 +40,7 @@ rest = context.JK.Rest()
</div>
<form className="school-signup-form">
<div className="field">
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
</div>
<div className="field">
<label>Password: </label><input type="password" name="password"/>
@ -54,12 +54,29 @@ rest = context.JK.Rest()
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
<p className="privacy-policy">
<div className="privacy-policy">
We will not share your email.<br/>See our <a href="/corp/privacy" target="_blank">privacy policy</a>.
</p>
</div>
</div>`
if this.props.school.education
explain = `<p>Please register here if you are a private music lesson teacher affiliated with
the {this.props.school.name} music program, and if you are interested in teaching online music lessons using the
JamKazam service. When you
have registered, someone from JamKazam will contact you to answer any questions you have about our online lesson
service. We'll help you get set up and ready to go, and we'll get into an online session with you to make sure
everything is working properly.</p>`
else
explain = `<p> Please register here if you are currently a teacher with {this.props.school.name}, and if you plan
to teach
online music lessons for students of {this.props.school.name} using the JamKazam service. When you have
registered, we
will
email you instructions to set up your online teacher profile, and we'll schedule a brief online training session
to make sure
you are comfortable using the service and ready to go with students in online lessons.</p>`
`<div className="container">
<div className="header-area">
<div className="header-content">
@ -75,13 +92,7 @@ rest = context.JK.Rest()
</div>
<div className="explain">
<p>
Please register here if you are currently a teacher with {this.props.school.name}, and if you plan to teach
online music lessons for students of {this.props.school.name} using the JamKazam service. When you have registered, we
will
email you instructions to set up your online teacher profile, and we'll schedule a brief online training session to make sure
you are comfortable using the service and ready to go with students in online lessons.
</p>
{explain}
</div>
{register}
@ -111,7 +122,7 @@ rest = context.JK.Rest()
else
@createUser()
@setState({processing:true})
@setState({processing: true})
createUser: () ->
$form = $('.school-signup-form')

View File

@ -0,0 +1,233 @@
context = window
rest = context.JK.Rest()
@SchoolTeacherListPage = React.createClass({
signupUrl: () ->
"/school/#{this.props.school.id}/student?redirect-to=#{encodeURIComponent(window.location.href)}"
render: () ->
loggedIn = context.JK.currentUserId?
if this.props.school.large_photo_url?
logo = `<div className="school-logo">
<img src={this.props.school.large_photo_url}/>
</div>`
if this.state.done
ctaButtonText = 'reloading page...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "SIGN UP"
else
ctaButtonText = "SIGN UP"
if loggedIn
register = `<div className={classNames({'cta-button' : true})}>ALREADY SIGNED UP</div>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="school-signup-form">
<label>Email: </label><input type="text" name="email"/>
<label>Password: </label><input type="password" name="password"/>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
</div>`
`<div className="container">
<div className="header-area">
<div className="header-content">
{logo}
<div className="headers">
<h1>OUR TEACHERS</h1>
<h2>at {this.props.school.name}</h2>
</div>
<div className="explain">
<p>
If you have not signed up to take private music lessons online using JamKazam, you can sign up using the form
on the right side of the page.
</p>
<p>
If you have already signed up and have set up your gear with help from the people at JamKazam, then you are
ready to book your lessons with a teacher.
You may book a lesson with one of your school's preferred instructors from the list below by clicking the BOOK
LESSON button next to your preferred instructor.
</p>
<p>
If your school does not have preferred instructors, or if your music program director has indicated that you
should find
and select your instructor from our broader community of teachers, then <a href='/client#/'>click this link</a> to use
our instructor search feature to find
a great instructor for you. If you need help, email us at <a href='mailto:support@jamkazam.com'>support@jamkazam.com</a>.
</p>
</div>
<br className="clearall"/>
</div>
</div>
<div className="preview-and-action-box jamclass school">
<div className="preview-jamtrack-header">
Sign Up For Lessons
</div>
<div className={classNames({'preview-area': true, 'jam-class': true})}>
<p>Sign up to let us know youre interested taking lessons online using JamKazam.</p>
<p>We'll follow up to answer all your questions, and to help you get set up and ready to go.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy
policy</a></p>
{register}
</div>
</div>
<div className="teacher-lister">
{this.list()}
</div>
<br className="clearall"/>
<br/>
</div>`
teaches: (teacher) ->
if teacher.instruments.length == 0
return ''
else if teacher.instruments.length == 2
return 'teaches ' + teacher.instruments[0].description + ' and ' + teacher.instruments[1].description
else
return 'teaches ' + teacher.instruments.map((i) -> i.description).join(', ')
list: () ->
teachers = []
teachersList = this.rabl.teachers
for teacher in teachersList
continue if !teacher.user?
teachers.push(`
<div className="school-teacher">
<div className="school-top-row">
<div className="school-left">
<div className="avatar">
<span className="vertalign">
<img src={teacher.user.resolved_photo_url}/>
</span>
</div>
<div className="book-lesson">
<span className="vertalign">
<a className="button-orange" onClick={this.bookLessonClicked.bind(this, teacher)} href={this.bookLessonUrl(teacher)}>BOOK LESSON</a>
</span>
</div>
</div>
<div className="school-right">
<div className="username">
<span className="vertalign">
<span className="teacher-descr">
{teacher.user.name} {this.teaches(teacher)}
<a className="profile-link" href={teacher.user.teacher_profile_url}>see {teacher.user.first_name}'s detailed instructor profile</a>
</span>
</span>
</div>
</div>
<br className="clearall"/>
</div>
</div>`)
teachers
bookLessonClicked: (teacher, e) ->
loggedIn = context.JK.currentUserId?
if loggedIn
# do nothing
else
e.preventDefault()
context.JK.Banner.showNotice('Please Sign Up', 'Before booking a lesson with a teacher, please sign up by filling out the sign up form on the right. Thank you!')
bookLessonUrl: (teacher) ->
'/client#/jamclass/book-lesson/normal_' + teacher.user.id
getInitialState: () ->
{loginErrors: null, processing: false}
componentWillMount: () ->
this.rabl = JSON.parse(this.props.rabl)
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{context.JK.currentUserId}"
else
@createUser()
@setState({processing: true})
createUser: () ->
$form = $('.school-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
student: true,
school_id: this.props.school.id
})
.done((response) =>
@setState({done: true})
#window.location.href = "/client#/jamclass"
window.location.reload()
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
})

View File

@ -21,8 +21,9 @@ teacherActions = window.JK.Actions.Teacher
lesson.me = me
lesson.other = other
lesson.isAdmin = context.JK.currentUserAdmin
lesson.schoolOnSchool = lesson['school_on_school?']
lesson.cardNotOk = !lesson.schoolOnSchool && !lesson.lesson_booking.card_presumed_ok
lesson.noSchoolOnSchoolPayment = lesson['payment_if_school_on_school??']
lesson.cardNotOk = !lesson.lesson_booking.card_presumed_ok && lesson.payment_if_school_on_school?
lesson.isActive = lesson['is_active?']
if (lesson.status == 'requested' || lesson.status == 'countered')
lesson.isRequested = true

View File

@ -41,8 +41,12 @@ rest = new context.JK.Rest()
onPick: () ->
rest.generateSchoolFilePickerPolicy({id: @target.id})
.done((filepickerPolicy) =>
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id})
genpolicy.done((filepickerPolicy) =>
@pickerOpen = true
@changed()
window.filepicker.setKey(gon.fp_apikey);
@ -69,7 +73,7 @@ rest = new context.JK.Rest()
.fail(@app.ajaxError)
afterImageUpload: (fpfile) ->
logger.debug("afterImageUploaded")
logger.debug("afterImageUploaded", typeof fpfile, fpfile)
$.cookie('original_fpfile', JSON.stringify(fpfile));
@currentFpfile = fpfile
@ -79,8 +83,12 @@ rest = new context.JK.Rest()
@signFpfile()
signFpfile: () ->
rest.generateSchoolFilePickerPolicy({ id: @target.id})
.done((policy) => (
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id})
genpolicy.done((policy) => (
@signedCurrentFpfile = @currentFpfile.url + '?signature=' + policy.signature + '&policy=' + policy.policy;
@changed()
))
@ -125,6 +133,8 @@ rest = new context.JK.Rest()
if @type == 'school'
window.SchoolActions.refresh()
if @type == 'retailer'
window.RetailerActions.refresh()
@app.layout.closeDialog('upload-avatar')
@ -184,7 +194,10 @@ rest = new context.JK.Rest()
@updatingAvatar = true
@changed()
rest.deleteSchoolAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
if @type == 'school'
rest.deleteSchoolAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
else if @type == 'retailer'
rest.deleteRetailerAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
deleteDone: (response) ->
@currentFpfile = null
@ -194,6 +207,8 @@ rest = new context.JK.Rest()
@currentCropSelection = null
if @type == 'school'
window.SchoolActions.refresh()
else if @type == 'retailer'
window.RetailerActions.refresh()
@app.layout.closeDialog('upload-avatar');
@ -219,8 +234,12 @@ rest = new context.JK.Rest()
logger.debug("Converting...");
fpfile = @determineCurrentFpfile();
rest.generateSchoolFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
.done((filepickerPolicy) =>
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
genpolicy.done((filepickerPolicy) =>
window.filepicker.setKey(gon.fp_apikey)
window.filepicker.convert(fpfile, {
crop: [
@ -243,8 +262,12 @@ rest = new context.JK.Rest()
scale: (cropped) ->
logger.debug("converting cropped");
rest.generateSchoolFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
.done((filepickerPolicy) => (
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
genpolicy.done((filepickerPolicy) => (
window.filepicker.convert(cropped, {
height: @targetCropSize,
width: @targetCropSize,
@ -275,14 +298,24 @@ rest = new context.JK.Rest()
updateServer: (scaledLarger, scaled, cropped) ->
logger.debug("converted and scaled final image %o", scaled);
rest.updateSchoolAvatar({
id: @target.id,
original_fpfile: @determineCurrentFpfile(),
cropped_fpfile: scaled,
cropped_large_fpfile: scaledLarger,
crop_selection: @selection
})
.done((response) => @updateAvatarSuccess(response))
if @type == 'school'
update = rest.updateSchoolAvatar({
id: @target.id,
original_fpfile: @determineCurrentFpfile(),
cropped_fpfile: scaled,
cropped_large_fpfile: scaledLarger,
crop_selection: @selection
})
else if @type == 'retailer'
update = rest.updateRetailerAvatar({
id: @target.id,
original_fpfile: @determineCurrentFpfile(),
cropped_fpfile: scaled,
cropped_large_fpfile: scaledLarger,
crop_selection: @selection
})
update.done((response) => @updateAvatarSuccess(response))
.fail(@app.ajaxError)
.always(() => (
@updatingAvatar = false

Some files were not shown because too many files have changed in this diff Show More