From 7ff7327ef8c0d8ea5e2c2f8e86b6330fc54c052a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 9 Jul 2017 21:21:29 -0500 Subject: [PATCH] lots of progress on amazon-readiness --- admin/Gemfile | 8 +- admin/app/admin/inactive_jamclass_users.rb | 40 ++++ admin/app/admin/inactive_posa_jamclass.rb | 45 ++++ admin/app/admin/lesson_session.rb | 23 +- ...ic_session_history.rb => music_session.rb} | 47 ++++ admin/app/admin/posa_card_upload.rb | 20 +- admin/app/admin/teachers.rb | 12 +- admin/app/admin/user_background_check.rb | 31 +++ admin/config/application.rb | 2 +- admin/config/initializers/jam_ruby_teacher.rb | 2 +- admin/public/404.html | 26 --- admin/public/422.html | 26 --- admin/public/500.html | 25 --- admin/public/favicon.ico | 0 admin/public/robots.txt | 5 - db/manifest | 1 + db/up/amazon_v1.sql | 38 ++++ ruby/lib/jam_ruby/app/mailers/admin_mailer.rb | 8 + ruby/lib/jam_ruby/connection_manager.rb | 2 +- ruby/lib/jam_ruby/lib/guitar_center.rb | 5 +- ruby/lib/jam_ruby/models/lesson_booking.rb | 103 ++++++++- .../models/lesson_package_purchase.rb | 43 +++- .../jam_ruby/models/lesson_package_type.rb | 32 ++- ruby/lib/jam_ruby/models/lesson_session.rb | 4 +- .../models/lesson_session_monthly_price.rb | 49 ++++- ruby/lib/jam_ruby/models/music_session.rb | 6 +- ruby/lib/jam_ruby/models/posa_card.rb | 91 ++++---- ruby/lib/jam_ruby/models/sale.rb | 13 +- ruby/lib/jam_ruby/models/teacher.rb | 6 +- .../jam_ruby/models/teacher_distribution.rb | 6 +- ruby/lib/jam_ruby/models/teacher_payment.rb | 56 +++-- ruby/lib/jam_ruby/models/user.rb | 64 ++++-- .../jam_ruby/resque/scheduled/hourly_job.rb | 1 + ruby/spec/factories.rb | 19 +- .../flows/monthly_recurring_lesson_spec.rb | 41 ++++ .../jam_ruby/flows/testdrive_lesson_spec.rb | 3 +- .../models/lesson_package_type_spec.rb | 24 +++ .../lesson_session_monthly_price_spec.rb | 17 +- ruby/spec/jam_ruby/models/posa_card_spec.rb | 44 ++-- ruby/spec/jam_ruby/models/sale_spec.rb | 2 + ruby/spec/jam_ruby/models/user_spec.rb | 4 +- ruby/spec/support/utilities.rb | 4 + .../javascripts/dialog/shutdownDialog.js | 4 + .../react-components/BookLesson.js.jsx.coffee | 23 +- .../JamClassScreen.js.jsx.coffee | 5 + .../LessonBooking.js.jsx.coffee | 42 ++-- .../LessonBookingDecision.js.jsx.coffee | 2 +- .../LessonPayment.js.jsx.coffee | 85 ++++---- .../TeacherProfile.js.jsx.coffee | 26 ++- .../TeacherSearchScreen.js.jsx.coffee | 9 +- .../landing/AccountActivatePage.js.jsx.coffee | 200 ++++++++++++++++++ .../landing/RedeemGiftCardPage.js.jsx.coffee | 26 ++- .../mixins/PostProcessorMixin.js.coffee | 10 +- .../react-components/TeacherProfile.scss | 8 + .../react-components/TeacherSearch.scss | 5 + .../landings/account_activate.scss | 90 ++++++++ .../api_music_sessions_controller.rb | 29 +++ web/app/controllers/api_users_controller.rb | 15 +- web/app/controllers/landings_controller.rb | 6 + web/app/views/api_lesson_bookings/show.rabl | 2 +- web/app/views/api_lesson_sessions/show.rabl | 2 +- web/app/views/api_stripe/store.rabl | 13 +- web/app/views/api_teachers/detail.rabl | 3 +- web/app/views/api_users/show.rabl | 2 +- .../views/api_users/show_teacher_index.rabl | 2 +- .../views/landings/account_activate.html.slim | 5 + web/config/application.rb | 1 + web/config/environments/development.rb | 1 + web/config/environments/test.rb | 2 +- web/config/routes.rb | 1 + web/lib/tasks/lesson.rake | 64 +++++- web/posa_cards.rb | 66 ++++++ .../api_jamblasters_controller_spec.rb | 11 +- web/spec/factories.rb | 36 +++- web/spec/features/activate_account_spec.rb | 126 +++++++++++ web/spec/features/book_test_drive_spec.rb | 2 +- web/spec/requests/users_controller_spec.rb | 11 +- web/spec/spec_helper.rb | 2 +- websocket-gateway/config/application.yml | 4 +- 79 files changed, 1586 insertions(+), 353 deletions(-) create mode 100644 admin/app/admin/inactive_jamclass_users.rb create mode 100644 admin/app/admin/inactive_posa_jamclass.rb rename admin/app/admin/{music_session_history.rb => music_session.rb} (61%) create mode 100644 admin/app/admin/user_background_check.rb delete mode 100644 admin/public/404.html delete mode 100644 admin/public/422.html delete mode 100644 admin/public/500.html delete mode 100644 admin/public/favicon.ico delete mode 100644 admin/public/robots.txt create mode 100644 db/up/amazon_v1.sql create mode 100644 ruby/spec/jam_ruby/models/lesson_package_type_spec.rb create mode 100644 web/app/assets/javascripts/react-components/landing/AccountActivatePage.js.jsx.coffee create mode 100644 web/app/assets/stylesheets/landings/account_activate.scss create mode 100644 web/app/views/landings/account_activate.html.slim create mode 100644 web/posa_cards.rb create mode 100644 web/spec/features/activate_account_spec.rb diff --git a/admin/Gemfile b/admin/Gemfile index c66e1b869..75632d35c 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -59,7 +59,7 @@ gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' gem 'resque-lonely_job', '~> 1.0.0' -gem 'eventmachine', '1.0.4' +gem 'eventmachine', '1.2.3' gem 'amqp', '0.9.8' #gem 'logging-rails', :require => 'logging/rails' gem 'pg_migrate' @@ -82,9 +82,9 @@ gem 'stripe' gem 'zip-codes' gem 'email_validator' -group :libv8 do - gem 'libv8', "~> 4.5.95" -end +#group :libv8 do +# gem 'libv8', "~> 4.5.95" +#end # To use Jbuilder templates for JSON diff --git a/admin/app/admin/inactive_jamclass_users.rb b/admin/app/admin/inactive_jamclass_users.rb new file mode 100644 index 000000000..c5e5bb9aa --- /dev/null +++ b/admin/app/admin/inactive_jamclass_users.rb @@ -0,0 +1,40 @@ +ActiveAdmin.register JamRuby::User, :as => 'InactiveJamClassUsers' do + + menu :label => 'Inactive JamClass Users w/o Credits', :parent => 'JamClass' + + config.sort_order = 'created_at' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.includes(:taken_lessons => :music_session).select("distinct(users.id), users.email, users.first_name, users.last_name").joins("inner join posa_cards on posa_cards.user_id = users.id inner join lesson_sessions on lesson_sessions.user_id = users.id left outer join music_sessions on music_sessions.lesson_session_id = music_sessions.id ").where("jamclass_credits = 0") } + + 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 + column "POSA" do |user| + span do + posa = user.posa_cards[0] + if posa.lesson_package_type + posa.lesson_package_type.id + else + posa.card_type + end + end + end + column "Last Session" do |user| + span do + if user.taken_lessons.length == 0 + "none yet" + else + most_recent_lesson = user.taken_lessons.order('created_at desc')[0] + link_to most_recent_lesson.scheduled_start, admin_lesson_session_path(most_recent_lesson) + end + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/inactive_posa_jamclass.rb b/admin/app/admin/inactive_posa_jamclass.rb new file mode 100644 index 000000000..5b86906e3 --- /dev/null +++ b/admin/app/admin/inactive_posa_jamclass.rb @@ -0,0 +1,45 @@ +ActiveAdmin.register JamRuby::User, :as => 'InactiveJamClassPOSAUsers' do + + menu :label => 'Inactive JamClass Users w/ Credits', :parent => 'JamClass' + + config.sort_order = 'created_at' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.includes(:taken_lessons => :music_session).select("distinct(users.id), users.email, users.first_name, users.last_name, users.jamclass_credits").joins("inner join posa_cards on posa_cards.user_id = users.id left outer join lesson_sessions on lesson_sessions.user_id = users.id left outer join music_sessions on music_sessions.lesson_session_id = music_sessions.id ").where("jamclass_credits > 0 AND music_sessions.id IS NULL OR music_sessions.scheduled_start < ?", Time.now - 7.days) } + + 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 + column "POSA" do |user| + span do + posa = user.posa_cards[0] + if posa.lesson_package_type + posa.lesson_package_type.id + else + posa.card_type + end + end + end + column "Credits" do |user| + span do + user.jamclass_credits + end + end + column "Last Session" do |user| + span do + if user.taken_lessons.length == 0 + "none yet" + else + most_recent_lesson = user.taken_lessons.order('created_at desc')[0] + link_to most_recent_lesson.scheduled_start, admin_lesson_session_path(most_recent_lesson) + end + end + end + end +end \ No newline at end of file diff --git a/admin/app/admin/lesson_session.rb b/admin/app/admin/lesson_session.rb index cd4458a43..4e781e524 100644 --- a/admin/app/admin/lesson_session.rb +++ b/admin/app/admin/lesson_session.rb @@ -17,10 +17,16 @@ ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do scope("Completed") { |scope| scope.unscoped.completed.order('created_at desc') } index do - column "User Link" do |lesson_session| + column "Actions" do |teacher| + links = ''.html_safe + links << link_to("View", resource_path(teacher), :class => "member_link view_link") + links << link_to("Edit", edit_resource_path(teacher), :class => "member_link edit_link") + links + end + column "App Link" do |lesson_session| lesson_booking = lesson_session.lesson_booking span do - link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + link_to "link", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" end end column "Status" do |lesson_session| @@ -58,10 +64,10 @@ ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do show do attributes_table do - row "User Link" do |lesson_session| + row "App Link" do |lesson_session| lesson_booking = lesson_session.lesson_booking span do - link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + link_to "link", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" end end row "Status" do |lesson_session| @@ -82,13 +88,13 @@ ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do row "Teacher" do |lesson_session| teacher = lesson_session.teacher span do - link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + link_to teacher.admin_name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" end end row "Student" do |lesson_session| student = lesson_session.student span do - link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + link_to student.admin_name, "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" end end row "Followup Emails Sent" do |lesson_session| @@ -111,6 +117,11 @@ ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do lesson_session.timed_description end end + row "Session" do |lesson_session| + span do + link_to "Session", lesson_session.music_session.admin_url + end + end row "Analysis" do |lesson_session| if lesson_session.analysed span style: "white-space: pre;" do diff --git a/admin/app/admin/music_session_history.rb b/admin/app/admin/music_session.rb similarity index 61% rename from admin/app/admin/music_session_history.rb rename to admin/app/admin/music_session.rb index c7eb5fcb7..80b5dafd5 100644 --- a/admin/app/admin/music_session_history.rb +++ b/admin/app/admin/music_session.rb @@ -60,4 +60,51 @@ ActiveAdmin.register JamRuby::MusicSession, :as => 'Music Session' do end end + show do + attributes_table do + row :id + row :name + row :description + row :creator do |session| + link_to(session.creator.admin_name, session.creator.admin_url) + end + row :created_at + row :started_at + row :session_ended_at do |session| session.session_removed_at end + row :genre + row :recurring_mode + row :timezone + row :fan_access + row :music_access + row :approval_required + row :open_rsvps + row :is_unstructured_rsv + row :canceled + row :lesson_session do |session| + lesson_session = session.lesson_session + if lesson_session + link_to("Lesson", lesson_session.admin_url) + else + '' + end + end + row 'Session Attendances' do |session| + + table_for(msuh = session.music_session_user_histories) do + column :user do |msuh| msuh.user.admin_name end + column :joined do |msuh| msuh.created_at.strftime('%b %d %Y, %H:%M') end + column :duration do |msuh| "#{msuh.duration_minutes.round} minutes" end + column :perf_data do |msuh| + unless (uu = msuh.perf_uri).blank? + link_to('Per Data Link', uu) + else + 'No Perf Data' + end + end + end + + end + end + end + end diff --git a/admin/app/admin/posa_card_upload.rb b/admin/app/admin/posa_card_upload.rb index 75353ca6a..784a325c3 100644 --- a/admin/app/admin/posa_card_upload.rb +++ b/admin/app/admin/posa_card_upload.rb @@ -10,15 +10,31 @@ ActiveAdmin.register_page "POSA Card Uploads" do 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" + if row.length != 4 + raise "UKNONWN CSV FORMAT! Must be 4 columns" end code = row[0] + lesson_package_type = row[1] + preactivate = row[2].strip == "true" + requires_purchase = row[3].strip == "true" posa_card = PosaCard.new posa_card.code = code + posa_card.lesson_package_type = LessonPackageType.find(lesson_package_type) + posa_card.preactivate = preactivate + posa_card.requires_purchase = requires_purchase + posa_card.purchased = !requires_purchase posa_card.card_type = params[:jam_ruby_posa_card][:card_type] + + + if posa_card.card_type == PosaCard::JAM_CLASS_4 + posa_card.is_lesson = true + posa_card.credits = 4 + elsif posa_card.card_type == PosaCard::JAM_CLASS_2 + posa_card.is_lesson = true + posa_card.credits = 2 + end posa_card.origin = file .original_filename posa_card.save! end diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb index e54718565..854fcaeae 100644 --- a/admin/app/admin/teachers.rb +++ b/admin/app/admin/teachers.rb @@ -45,7 +45,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end -=begin + column "Background Check" do |teacher| div do if teacher.background_check_at @@ -56,25 +56,25 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do br end span do - link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do - "mark as checked" + link_to(edit_admin_teacher_background_check_path(teacher.id)) do + "update background check" end end else span do - '' + 'NO' end span do br end span do - link_to("mark as checked", mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) + link_to("update background check", edit_admin_teacher_background_check_path(teacher.id)) end end end end -=end + column "Session Ready" do |teacher| div do if teacher.ready_for_session_at diff --git a/admin/app/admin/user_background_check.rb b/admin/app/admin/user_background_check.rb new file mode 100644 index 000000000..0d112ee23 --- /dev/null +++ b/admin/app/admin/user_background_check.rb @@ -0,0 +1,31 @@ +ActiveAdmin.register JamRuby::Teacher, :as => 'TeacherBackgroundCheck' do + + + config.filters = false + menu :label => 'Teacher Background Check', :parent => 'JamClass' + + + + form do |f| + f.inputs 'Set Background Check' do + f.input :background_check_at, as: :date_select + end + f.actions + end + + + index do + column "Actions" do |teacher| + links = ''.html_safe + links << link_to("View", resource_path(teacher), :class => "member_link view_link") + links << link_to("Edit", edit_resource_path(teacher), :class => "member_link edit_link") + links + end + + column 'User' do |oo| + oo.user.email + end + end + + +end diff --git a/admin/config/application.rb b/admin/config/application.rb index 04ac0ef94..e9b2f6d4f 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -87,7 +87,7 @@ module JamAdmin config.recurly_root_url = 'https://jamkazam-development.recurly.com' # where is rabbitmq? - config.rabbitmq_host = "localhost" + config.rabbitmq_host = "127.0.0.1" config.rabbitmq_port = 5672 # set to false to instead use amazon. You will also need to supply amazon secrets diff --git a/admin/config/initializers/jam_ruby_teacher.rb b/admin/config/initializers/jam_ruby_teacher.rb index f855ae238..5a9cf3442 100644 --- a/admin/config/initializers/jam_ruby_teacher.rb +++ b/admin/config/initializers/jam_ruby_teacher.rb @@ -1,5 +1,5 @@ class JamRuby::Teacher - attr_accessible :short_bio, as: :admin + attr_accessible :short_bio, :background_check_at, as: :admin end diff --git a/admin/public/404.html b/admin/public/404.html deleted file mode 100644 index 9a48320a5..000000000 --- a/admin/public/404.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - -
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
- - diff --git a/admin/public/422.html b/admin/public/422.html deleted file mode 100644 index 83660ab18..000000000 --- a/admin/public/422.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - -
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
- - diff --git a/admin/public/500.html b/admin/public/500.html deleted file mode 100644 index f3648a0db..000000000 --- a/admin/public/500.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - -
-

We're sorry, but something went wrong.

-
- - diff --git a/admin/public/favicon.ico b/admin/public/favicon.ico deleted file mode 100644 index e69de29bb..000000000 diff --git a/admin/public/robots.txt b/admin/public/robots.txt deleted file mode 100644 index 085187fa5..000000000 --- a/admin/public/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-Agent: * -# Disallow: / diff --git a/db/manifest b/db/manifest index 5d4ca82e9..37a272648 100755 --- a/db/manifest +++ b/db/manifest @@ -376,3 +376,4 @@ jam_track_download_rights.sql guitar_center_integration_v1.sql mobile_recording_support.sql youtube_broadcast.sql +amazon_v1.sql \ No newline at end of file diff --git a/db/up/amazon_v1.sql b/db/up/amazon_v1.sql new file mode 100644 index 000000000..c517d3feb --- /dev/null +++ b/db/up/amazon_v1.sql @@ -0,0 +1,38 @@ +-- create new lesson package types + +ALTER TABLE lesson_package_types ADD COLUMN is_test_drive BOOLEAN NOT NULL DEFAULT TRUE; +UPDATE lesson_package_types SET is_test_drive = FALSE WHERE id in('single', 'single-free'); + +INSERT INTO lesson_package_types (id, name, description, package_type, price, is_test_drive) VALUES ('amazon-test-drive-free-4', 'Test Drive (4)', 'Four free lessons which you can use to find that ideal teacher.', 'test-drive-4', 0.0, TRUE); +INSERT INTO lesson_package_types (id, name, description, package_type, price, is_test_drive) VALUES ('amazon-test-drive-free-2', 'Test Drive (2)', 'Two free lessons which you can use to find that ideal teacher.', 'test-drive-2', 0.0, TRUE); +INSERT INTO lesson_package_types (id, name, description, package_type, price, is_test_drive) VALUES ('amazon-test-drive-paid-4', 'Test Drive (4)', 'Four reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-4', 19.99, TRUE); + + +-- need new posa card types for Amazon -- should be able to point to lesson package type +ALTER TABLE posa_cards ADD COLUMN lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id) ON DELETE SET NULL; +ALTER TABLE posa_cards ADD COLUMN credits INTEGER NOT NULL DEFAULT 1; +ALTER TABLE posa_cards ADD COLUMN is_lesson BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE posa_cards ADD COLUMN preactivate BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE posa_cards ADD COLUMN requires_purchase BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE posa_cards ADD COLUMN purchased BOOLEAN NOT NULL DEFAULT TRUE; + +ALTER TABLE lesson_bookings ADD COLUMN posa_card_purchased BOOLEAN NOT NULL DEFAULT TRUE; + +update posa_cards set credits = 5 where card_type = 'jam_tracks_5'; +update posa_cards set credits = 10 where card_type = 'jam_tracks_10'; +update posa_cards set credits = 4 where card_type = 'jam_class_4'; +update posa_cards set is_lesson = TRUE where card_type = 'jam_class_4'; +update posa_cards set lesson_package_type_id = 'test-drive' where card_type = 'jam_class_4'; + +ALTER TABLE lesson_package_purchases ADD COLUMN total_roll_forward_amount_in_cents INTEGER; +ALTER TABLE lesson_package_purchases ADD COLUMN remaining_roll_forward_amount_in_cents INTEGER; +ALTER TABLE lesson_package_purchases ADD COLUMN reduced_roll_forward_amount_in_cents INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE lesson_package_purchases ADD COLUMN expected_session_times INTEGER; +ALTER TABLE lesson_package_purchases ADD COLUMN actual_session_times INTEGER; +ALTER TABLE teacher_distributions ADD COLUMN reduced_roll_forward_amount_in_cents INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE lesson_bookings ADD COLUMN remaining_roll_forward_amount_in_cents INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE users ADD COLUMN lesson_package_needs_purchase_id VARCHAR(64) REFERENCES lesson_package_types(id) ON DELETE SET NULL; + diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb index 993b9f81b..79f5c2c0f 100644 --- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -20,6 +20,14 @@ module JamRuby subject: options[:subject]) end + def jamclass_alerts(options) + mail(to: APP_CONFIG.email_jamclass_alerts_alias, + from: APP_CONFIG.email_generic_from, + body: options[:body], + content_type: "text/plain", + subject: options[:subject]) + end + def crash_alert(options) mail(to: APP_CONFIG.email_crashes_alias, from: APP_CONFIG.email_generic_from, diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 92aea6228..a540bab41 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -355,7 +355,7 @@ SQL if active_music_session music_session = active_music_session.music_session - if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller) + if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller.id) # find next in line, because the current 'session controller' is not part of the session next_in_line(music_session, active_music_session) end diff --git a/ruby/lib/jam_ruby/lib/guitar_center.rb b/ruby/lib/jam_ruby/lib/guitar_center.rb index bfbd34526..a722eec7f 100644 --- a/ruby/lib/jam_ruby/lib/guitar_center.rb +++ b/ruby/lib/jam_ruby/lib/guitar_center.rb @@ -88,13 +88,12 @@ module JamRuby # If this is a lesson posa card, then put that user into the guitar center school def self.post_posa_claim(posa) if posa.is_lesson_posa_card? + posa.user.is_a_student = true # Associate user with guitar center school - if posa.retailer.is_guitar_center? - posa.user.is_a_student = true + if posa.retailer && posa.retailer.is_guitar_center? if posa.user.school_id.nil? posa.user.school_id = School.guitar_center.id end - posa.user.save end end diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 60572f61b..415b54a0b 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -9,7 +9,7 @@ module JamRuby @@log = Logging.logger[LessonBooking] - attr_accessor :accepting, :countering, :canceling, :autocanceling, :countered_slot, :countered_lesson, :current_purchase, :current_lesson + attr_accessor :accepting, :countering, :canceling, :autocanceling, :countered_slot, :countered_lesson, :current_purchase, :current_lesson, :expected_session_times, :adjustment_in_cents STATUS_REQUESTED = 'requested' STATUS_CANCELED = 'canceled' @@ -97,7 +97,7 @@ module JamRuby end def after_create - if (posa_card || card_presumed_ok || !payment_if_school_on_school?) && !sent_notices + if ((posa_card && posa_card.purchased) || card_presumed_ok || !payment_if_school_on_school?) && !sent_notices send_notices end end @@ -188,7 +188,7 @@ module JamRuby success = self.save if !success - puts "unable to accept lesson booking #{errors.inspect}" + #puts "unable to accept lesson booking #{errors.inspect}" end success end @@ -467,6 +467,11 @@ module JamRuby def resolved_test_drive_package result = nil + # posa card is best indicator of lesson package type + if posa_card + return posa_card.lesson_package_type + end + purchase = student.most_recent_test_drive_purchase if purchase # for lessons already packaged @@ -528,21 +533,22 @@ module JamRuby end end - def distribution_price_in_cents(target, education, split = nil, fee_rate = nil) - distribution = teacher_distribution_price_in_cents(target) + def distribution_price_in_cents(target, education, split = nil) + if split + distribution = teacher_distribution_price_in_cents(target, split) (distribution * split).round - - # when a split is provided, we also pin down the teacher_fee_in_cents, instead of assuming a bunch of stuff elsif education + distribution = teacher_distribution_price_in_cents(target, 0.0625) (distribution * 0.0625).round # 0.0625 is 1/4th of 25% else + distribution = teacher_distribution_price_in_cents(target) distribution end end - def teacher_distribution_price_in_cents(target) + def teacher_distribution_price_in_cents(target, split = nil) if is_single_free? 0 elsif is_test_drive? @@ -558,13 +564,76 @@ module JamRuby # we are in the month being billed. we should set the start date based on today start_date = today end - (LessonSessionMonthlyPrice.price(self, start_date) * 100).round + price, times = LessonSessionMonthlyPrice.price(self, start_date) + + price_in_cents = (price * 100).round + # OK, we have a suggested price based on date, but we need to now adjust if previous lessons have been unsuccessful + + adjusted_price_in_cents = LessonSessionMonthlyPrice.adjust_for_missed_lessons(self, price_in_cents, split) + + self.expected_session_times = times # save for later + self.adjustment_in_cents = price_in_cents - adjusted_price_in_cents + adjusted_price_in_cents else booked_price * 100 end end end + # find any lesson package purchases for this lesson booking for previous months that have not had adjustments + # we have to base this on 'now', meaning we do not consider months until they are fully closed + # this does mean, because we collect up to a week in advance of a new month starting, that a student will likely not see adjustments until 2 cycles forward + def self.previous_needing_adjustment + now = Time.now.utc + year = now.year + month = now.month + + if month == 1 + previous_year = year - 1 + previous_month = 12 + else + previous_year = year + previous_month = month - 1 + end + + LessonPackagePurchase.where(recurring: true).where('month <= ?', previous_month).where('year <= ?', previous_year).where('actual_session_times is null').where('expected_session_times is not null') + .limit(500) + end + + def self.adjust_for_missed_sessions + + # Go to previous lesson_package_purchase month, and see if we need to adjust for a roll forward due to missed sessions + previous_purchases = LessonBooking.previous_needing_adjustment + + previous_purchases.each do |previous_purchase| + + # XXX other monthly code uses session start time. should we be doing that? + successful_lessons = LessonSession.where(lesson_booking: previous_purchase.lesson_booking).where(success: true).where('analysed_at >= ? AND analysed_at < ?', previous_purchase.beginning_of_month_at, previous_purchase.end_of_month_at) + + previous_purchase.actual_session_times = successful_lessons.count + # find out how many actual lessons were had, and then we can adjust price of this current distribution (amount_in_cents) accordingly + + ratio = previous_purchase.actual_session_times.to_f / previous_purchase.expected_session_times.to_f + + if ratio < 1 + # discount next month for student + amount_paid_last_month_in_cents = previous_purchase.price_in_cents # this does not include tax. It's just the expected price of the booking + previous_purchase.remaining_roll_forward_amount_in_cents = previous_purchase.total_roll_forward_amount_in_cents = (amount_paid_last_month_in_cents * ratio).round + + # if there is a roll forward, add it to the lesson booking + previous_purchase.lesson_booking.remaining_roll_forward_amount_in_cents += previous_purchase.remaining_roll_forward_amount_in_cents + previous_purchase.lesson_booking.save! + + else + previous_purchase.total_roll_forward_amount_in_cents = 0 + previous_purchase.applied_roll_forward_amount_in_cents = 0 + end + + previous_purchase.save + end + end + + def is_single_free? lesson_type == LESSON_TYPE_FREE end @@ -674,6 +743,9 @@ module JamRuby def card_approved self.card_presumed_ok = true + if posa_card_id + self.posa_card_purchased = true + end if self.save && !sent_notices send_notices end @@ -792,7 +864,10 @@ module JamRuby 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 + lesson_booking.posa_card = user.most_recent_posa_card + if lesson_booking.posa_card + lesson_booking.posa_card_purchased = lesson_booking.posa_card.purchased + end 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 @@ -837,7 +912,7 @@ module JamRuby end def self.unprocessed(current_user) - LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where(same_school_free: false).where(posa_card:nil) + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).where(same_school_free: false).where('posa_card_id is null OR (posa_card_id is not null AND posa_card_purchased = false)') end def self.requested(current_user) @@ -902,9 +977,15 @@ module JamRuby # check for any recurring sessions where there are not at least 2 sessions into the future. If not, we need to make sure they get made def self.hourly_check schedule_upcoming_lessons + + # order matters: bill_monthly code will use the adjustments made in here to correct billing roll forward + adjust_for_missed_sessions + + # needs to come after 'adjust_for_missed_sessions' bill_monthlies end + def self.bill_monthlies now = Time.now billable_monthlies(now).each do |lesson_booking| diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb index 384358422..481e108cd 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -29,14 +29,24 @@ module JamRuby after_create :add_test_drives after_create :create_charge + + + 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 + if posa_card 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") + + if lesson_package_type.is_test_drive? + if user.lesson_package_needs_purchase_id + # if lesson_package_needs_purchase is set, we need to let the purchase go through because the user alrady has the credits; gotta let them pay + return + end + if !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 end end end @@ -61,7 +71,7 @@ module JamRuby end def add_test_drives - if posa_card_id + if posa_card #user.jamclass_credits incremented in posa_card.rb return end @@ -85,12 +95,28 @@ module JamRuby lesson_payment_charge.amount_in_cents / 100.0 end + def beginning_of_month_at + Date.new(year, month, 1).to_time.utc.beginning_of_month + end + + def end_of_month_at + Date.new(year, month, 1).to_time.utc.end_of_month + end + + def period_over? + Time.now.utc > end_of_month_at + end + 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 !purchase.posa_card && lesson_booking + # the lesson booking has a posa card? if so, we should track that + purchase.posa_card = lesson_booking.posa_card + end if year purchase.year = year @@ -117,6 +143,7 @@ module JamRuby purchase.teacher_distributions << teacher_dist # price should always match the teacher_distribution, if there is one purchase.price = teacher_dist.amount_in_cents / 100 + purchase.reduced_roll_forward_amount_in_cents += teacher_dist.reduced_roll_forward_amount_in_cents end if retailer_split && retailer_split > 0 @@ -124,33 +151,39 @@ module JamRuby teacher_dist.retailer = teacher.teacher.retailer teacher_dist.save purchase.teacher_distributions << teacher_dist + purchase.reduced_roll_forward_amount_in_cents += teacher_dist.reduced_roll_forward_amount_in_cents end else teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false) purchase.teacher_distributions << teacher_dist + purchase.reduced_roll_forward_amount_in_cents += teacher_dist.reduced_roll_forward_amount_in_cents # price should always match the teacher_distribution, if there is one purchase.price = teacher_dist.amount_in_cents / 100 if lesson_booking.school_on_school_payment? && lesson_booking.school.education teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, true) purchase.teacher_distributions << teacher_dist + purchase.reduced_roll_forward_amount_in_cents += teacher_dist.reduced_roll_forward_amount_in_cents end end + + # record expected times for times played in the month + purchase.expected_session_times = lesson_booking.expected_session_times end else purchase.recurring = false end if lesson_booking + purchase.lesson_package_type = lesson_package_type ? lesson_package_type : lesson_booking.lesson_package_type purchase.price = lesson_booking.booked_price if purchase.price.nil? else purchase.lesson_package_type = lesson_package_type purchase.price = lesson_package_type.price if purchase.price.nil? end - purchase.save purchase end diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb index 5424763fd..7c93e3d98 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_type.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -6,11 +6,16 @@ module JamRuby PRODUCT_TYPE = 'LessonPackageType' + TEST_DRIVE_4_ID = 'test-drive' + SINGLE_FREE = 'single-free' - TEST_DRIVE_4 = 'test-drive' + TEST_DRIVE_4 = 'test-drive-4' TEST_DRIVE_2 = 'test-drive-2' TEST_DRIVE_1 = 'test-drive-1' SINGLE = 'single' + AMAZON_TEST_DRIVE_4_PAID_ID = 'amazon-test-drive-paid-4' + AMAZON_TEST_DRIVE_FREE_2_ID = 'amazon-test-drive-free-2' + AMAZON_TEST_DRIVE_FREE_4_ID = 'amazon-test-drive-free-4' LESSON_PACKAGE_TYPES = [ @@ -22,14 +27,18 @@ module JamRuby ] has_many :user_desired_packages, class_name: "JamRuby::User", :foreign_key => "lesson_package_type_id", inverse_of: :desired_package + has_many :users_needing_purchase, class_name: "JamRuby::User", :foreign_key => "lesson_package_needs_purchase_id", inverse_of: :lesson_package_needs_purchase + has_many :posa_cards, class_name: "JamRuby::PosaCard" + validates :name, presence: true validates :description, presence: true validates :price, presence: true validates :package_type, presence: true, inclusion: {in: LESSON_PACKAGE_TYPES} def self.test_drive_package_ids - [TEST_DRIVE_4, TEST_DRIVE_2, TEST_DRIVE_1] + LessonPackageType.select("id").where(is_test_drive: true).map {|i| i.id } end + def self.monthly LessonPackageType.find(MONTHLY) end @@ -38,8 +47,20 @@ module JamRuby LessonPackageType.find(SINGLE_FREE) end + def self.amazon_test_drive_free_4 + LessonPackageType.find(AMAZON_TEST_DRIVE_FREE_4_ID) + end + + def self.amazon_test_drive_free_2 + LessonPackageType.find(AMAZON_TEST_DRIVE_FREE_2_ID) + end + + def self.amazon_test_drive_paid_4 + LessonPackageType.find(AMAZON_TEST_DRIVE_4_PAID_ID) + end + def self.test_drive_4 - LessonPackageType.find(TEST_DRIVE_4) + LessonPackageType.find(TEST_DRIVE_4_ID) end def self.test_drive_2 @@ -54,6 +75,7 @@ module JamRuby LessonPackageType.find(SINGLE) end + def booked_price(lesson_booking) if is_single_free? 0 @@ -94,7 +116,7 @@ module JamRuby end def is_test_drive? - id.start_with?('test-drive') + is_test_drive end def is_normal? @@ -112,7 +134,7 @@ module JamRuby def plan_code if package_type == SINGLE_FREE "lesson-package-single-free" - elsif package_type == 'test-drive-4' + elsif package_type == TEST_DRIVE_4 "lesson-package-test-drive-4" elsif package_type == TEST_DRIVE_2 "lesson-package-test-drive-2" diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index dc080d4a5..55fa41c6e 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -11,7 +11,7 @@ module JamRuby @@log = Logging.logger[LessonSession] delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true - delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :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 :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, :remaining_roll_forward_amount_in_cents, to: :lesson_booking delegate :pretty_scheduled_start, to: :music_session @@ -712,7 +712,7 @@ module JamRuby end # 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) + query = query.where('(lesson_bookings.posa_card_id IS NOT NULL AND lesson_bookings.posa_card_purchased) 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 diff --git a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb index b2eae100c..b90345d21 100644 --- a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb +++ b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb @@ -6,6 +6,7 @@ module JamRuby raise "lesson_booking is not monthly paid #{lesson_booking.admin_url}" if !lesson_booking.is_monthly_payment? + data = lesson_booking.predicted_times_for_month(start_day.year, start_day.month) times = data[:times] @@ -33,7 +34,53 @@ module JamRuby result = lesson_booking.booked_price end - result + [result, times.length] + end + + def self.adjust_for_missed_lessons(lesson_booking, price_in_cents, split) + + if !split + split = 1.0 + end + + adjusted_price_in_cents = price_in_cents + + lesson_package_purchases = LessonPackagePurchase.where(lesson_booking: lesson_booking).where('remaining_roll_forward_amount_in_cents > 0').order(:created_at) + remaining_roll_forward = 0 + skip_crediting = false + reduced_amount = 0 + + lesson_package_purchases.each do |lesson_package_purchase| + + if !skip_crediting + + take_off = (lesson_package_purchase.remaining_roll_forward_amount_in_cents * split).round + + amount_remaining_after_adjustment = price_in_cents - take_off + + if amount_remaining_after_adjustment <= 0 + # there isn't enough 'price_in_cents' to eat up the needed credit, so we say this teacher_distribution has no due price, and break out of the loo + adjusted_price_in_cents = 0 + reduced_amount += price_in_cents + lesson_package_purchase.remaining_roll_forward_amount_in_cents -= price_in_cents + skip_crediting = true + else + adjusted_price_in_cents = amount_remaining_after_adjustment + reduced_amount += take_off + # this lesson_package_purchase is now cleared out - totally credited! + lesson_package_purchase.remaining_roll_forward_amount_in_cents = 0 + end + end + + remaining_roll_forward = remaining_roll_forward + lesson_package_purchase.remaining_roll_forward_amount_in_cents + + lesson_package_purchase.save! + end + + lesson_booking.remaining_roll_forward_amount_in_cents = remaining_roll_forward + lesson_booking.save! + + adjusted_price_in_cents end end end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index a36c41987..4f965a4a6 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -126,7 +126,6 @@ module JamRuby def refresh_stream(user, broadcast) stream_data = get_livestream(user) - puts "REFRESH STREAM #{stream_data}" broadcast.stream_id = stream_data["id"] broadcast.stream_status = stream_data["status"]["streamStatus"] broadcast.stream_name = stream_data["cdn"]["ingestionInfo"]["streamName"] @@ -185,7 +184,6 @@ module JamRuby if livestream # if livestream["status"]["streamStatus"] == "active" - puts "LI EVESATREMA STATATUESNUHOENTUHENOSTHU #{livestream["status"]["streamStatus"]}" transition_broadcast(user, broadcast, 'live', google_client) # end else @@ -1254,6 +1252,10 @@ SQL sessions end + def admin_url + APP_CONFIG.admin_root_url + "/admin/music_sessions/" + id + end + private def generate_share_token diff --git a/ruby/lib/jam_ruby/models/posa_card.rb b/ruby/lib/jam_ruby/models/posa_card.rb index 68fe8d11a..604dee8fe 100644 --- a/ruby/lib/jam_ruby/models/posa_card.rb +++ b/ruby/lib/jam_ruby/models/posa_card.rb @@ -7,20 +7,25 @@ module JamRuby JAM_TRACKS_5 = 'jam_tracks_5' JAM_TRACKS_10 = 'jam_tracks_10' JAM_CLASS_4 = 'jam_class_4' + JAM_CLASS_2 = 'jam_class_2' CARD_TYPES = [ JAM_TRACKS_5, JAM_TRACKS_10, - JAM_CLASS_4 + JAM_CLASS_4, + JAM_CLASS_2 ] belongs_to :user, class_name: "JamRuby::User" belongs_to :retailer, class_name: "JamRuby::Retailer" + belongs_to :lesson_package_type, class_name: "JamRuby::LessonPackageType" + 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 @@ -34,20 +39,9 @@ module JamRuby validate :within_one_year def is_lesson_posa_card? - card_type == JAM_CLASS_4 + self.is_lesson 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? @@ -60,7 +54,7 @@ module JamRuby end def within_one_year - if user && claimed_at && claimed_at_was && claimed_at_changed? + 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 @@ -86,7 +80,7 @@ module JamRuby end def must_be_activated - if claimed_at && !activated_at + if claimed_at && !preactivate && !activated_at self.errors.add(:activated_at, 'must already be set') end end @@ -94,11 +88,11 @@ module JamRuby def check_attributed if user && user_id_changed? if card_type == JAM_TRACKS_5 - user.gifted_jamtracks += 5 + user.gifted_jamtracks += credits elsif card_type == JAM_TRACKS_10 - user.gifted_jamtracks += 10 - elsif card_type == JAM_CLASS_4 - user.jamclass_credits += 4 + user.gifted_jamtracks += credits + elsif is_lesson_posa_card? + user.jamclass_credits += credits else raise "unknown card type #{card_type}" end @@ -106,18 +100,6 @@ module JamRuby 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 @@ -128,9 +110,9 @@ module JamRuby 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' + elsif is_lesson_posa_card? + price = lesson_package_type.price + plan_code = "posa-jamclass-#{credits}" else raise "unknown card type #{card_type}" end @@ -148,20 +130,53 @@ module JamRuby self.save end + def has_been_purchased(create_purchase) + if !purchased + + #release flag on user account indicating they need to buy this card + user.lesson_package_needs_purchase = nil + user.save + + # indicate this has been purchased + self.purchased = true + self.save + + if is_lesson_posa_card? && create_purchase + LessonPackagePurchase.create(user, nil, lesson_package_type, nil, nil, self) + end + end + end + def claim(user) self.user = user self.claimed_at = Time.now if self.save + if user.errors.any? + # happens on signup if bad email etc + return + end + UserWhitelist.card_create(user, 'posa') - SaleLineItem.associate_user_for_posa(self, user) + + if !preactivate + SaleLineItem.associate_user_for_posa(self, user) + end + + if requires_purchase + # this is a flag on the account that makes it so the user can't buy any TD's on the payment page until they clear this up, because we've given them credits + user.lesson_package_needs_purchase = self.lesson_package_type + user.save + end # when you claim a POSA card, you are also making a LessonPackagePurchase if is_lesson_posa_card? GuitarCenter.post_posa_claim(self) - purchase = LessonPackagePurchase.create(user, nil, lesson_package_type, nil, nil, self) if purchase.nil? + if purchased + LessonPackagePurchase.create(user, nil, lesson_package_type, nil, nil, self) + end end end end @@ -171,8 +186,8 @@ module JamRuby 'JT-5' elsif card_type == JAM_TRACKS_10 'JT-10' - elsif card_type == JAM_CLASS_4 - 'JC-4' + elsif is_lesson_posa_card? + "JC-#{credits}" else raise "unknown card type #{card_type}" end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 97a633e90..c71726434 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -233,8 +233,8 @@ module JamRuby free && non_free end - def self.purchase_test_drive(current_user, lesson_package_type, booking = nil) - self.purchase_lesson(nil, current_user, booking, lesson_package_type) + def self.purchase_test_drive(current_user, lesson_package_type, booking = nil, posa_card = nil) + self.purchase_lesson(nil, current_user, booking, lesson_package_type, nil, nil, false, posa_card) end def self.post_sale_test_failure @@ -261,7 +261,7 @@ module JamRuby 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) + def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false, posa_card = nil) stripe_charge = nil sale = nil purchase = nil @@ -279,7 +279,7 @@ module JamRuby 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) + price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force, posa_card) post_sale_test_failure @@ -310,7 +310,7 @@ module JamRuby {sale: sale, stripe_charge: stripe_charge, purchase: purchase} end - def self.charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false) + def self.charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false, posa_card = nil) if lesson_package_purchase target = lesson_package_purchase elsif lesson_session @@ -322,10 +322,9 @@ module JamRuby current_user.sync_stripe_customer purchase = lesson_package_purchase - purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil? + purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type, nil, nil, posa_card) if purchase.nil? if purchase.errors.any? - puts "purchase errors #{purchase.errors.inspect}" price_info = {} price_info[:purchase] = purchase return price_info diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index 20eae0864..420311e56 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -141,7 +141,7 @@ module JamRuby query = query.where("teaches_age_lower <= ? AND (CASE WHEN teaches_age_upper = 0 THEN true ELSE teaches_age_upper >= ? END)", student_age, student_age) end - # don't show phantom teachers that teach 'bass guitar', 'acoustic guitar', 'electric_guitar' + # don't show phantom teachers that teach 'bass guitar', 'acoustic guitar', 'electric guitar' query = query.where("((select count(checkgt.instrument_id) from teachers_instruments checkgt where checkgt.teacher_id = teachers.id AND checkgt.instrument_id IN ('bass guitar', 'acoustic guitar', 'electric guitar') ) = 0 AND phantom = true) OR phantom = false") # order in this way: https://jamkazam.atlassian.net/browse/VRFS-4058 @@ -349,8 +349,8 @@ module JamRuby reviews.order('created_at desc').limit(20) end - def mark_background_checked - self.background_check_at = Time.now + def mark_background_checked(time) + self.background_check_at = time self.save! end diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 0ad3e9856..4c99bca3e 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -55,14 +55,14 @@ module JamRuby end def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education, split = nil, fee_rate = nil) + distribution = create(lesson_package_purchase, for_education, split, fee_rate) distribution.lesson_package_purchase = lesson_package_purchase distribution.education = for_education - # lock down the teacher_fee_in_cents distribution.teacher_fee_in_cents = distribution.calculate_teacher_fee(split, fee_rate) - + distribution.reduced_roll_forward_amount_in_cents = lesson_package_purchase.lesson_booking.adjustment_in_cents distribution end @@ -71,7 +71,7 @@ module JamRuby distribution.teacher = target.teacher distribution.ready = false distribution.distributed = false - distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education, split, fee_rate) + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education, split) distribution.school = target.lesson_booking.school distribution end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb index 592ba99b7..66ae5222d 100644 --- a/ruby/lib/jam_ruby/models/teacher_payment.rb +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -37,7 +37,7 @@ module JamRuby pending_teacher_payments.each do |row| teacher = User.find(row['id']) - TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).each do |distribution| + TeacherDistribution.where(teacher_id: teacher.id).where(ready: true).where(distributed: false).each do |distribution| payment = TeacherPayment.charge(teacher) if payment.nil? || !payment.teacher_payment_charge.billed break @@ -62,6 +62,7 @@ module JamRuby def last_billed_at_date teacher_payment_charge.last_billed_at_date end + def charge_retry_hours 24 end @@ -82,7 +83,7 @@ module JamRuby end if payment.teacher_distribution.nil? - teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).order(:created_at).first + teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready: true).where(distributed: false).order(:created_at).first if teacher_distribution.nil? return end @@ -90,34 +91,45 @@ module JamRuby end payment.school = payment.teacher_distribution.school - payment.amount_in_cents = payment.teacher_distribution.amount_in_cents + payment.amount_in_cents = payment.teacher_distribution.amount_in_cents payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee - effective_in_cents = payment.real_distribution_in_cents + if payment.teacher_distribution.amount_in_cents > 0 - if payment.teacher_payment_charge.nil? - charge = TeacherPaymentCharge.new - charge.user = payment.payable_teacher - 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! + effective_in_cents = payment.real_distribution_in_cents + + if payment.teacher_payment_charge.nil? + charge = TeacherPaymentCharge.new + charge.user = payment.payable_teacher + 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 = (effective_in_cents / (1 - APP_CONFIG.stripe[:ach_pct])).round + charge.fee_in_cents = payment.fee_in_cents + charge.save! + end + + payment.save! + + payment.teacher_payment_charge.charge + + if payment.teacher_payment_charge.billed + payment.teacher_distribution.distributed = true + payment.teacher_distribution.save! + end else - charge = payment.teacher_payment_charge - 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 - payment.save! - - payment.teacher_payment_charge.charge - - if payment.teacher_payment_charge.billed + # 0 amount distribution; these occur in 100% roll forward scenarios (previous month was completely missed) + payment.save! payment.teacher_distribution.distributed = true payment.teacher_distribution.save! + end + payment end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 55c328325..6db0617d0 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -41,7 +41,7 @@ module JamRuby attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection # updating_password corresponds to a lost_password - attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card + attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' has_many :controlled_sessions, :class_name => "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id @@ -82,7 +82,7 @@ module JamRuby has_many :playing_claimed_recordings, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :claimed_recording_initiator has_many :playing_jam_tracks, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :jam_track_initiator - # VRFS-2916 jam_tracks.id is varchar: REMOVE + # VRFS-2916 jam_tracks.id is varchar: REMOVE # has_many :jam_tracks_played, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id', :conditions => "jam_track_id IS NOT NULL" # VRFS-2916 jam_tracks.id is varchar: ADD has_many :jam_tracks_played, -> { where("playable_type = 'JamRuby::JamTrack'") }, :class_name => "JamRuby::PlayablePlay", :foreign_key => 'player_id' @@ -178,7 +178,7 @@ module JamRuby has_many :test_drive_package_choice_teachers, :class_name => "JamRuby::TestDrivePackageChoiceTeacher", :foreign_key => "teacher_id" has_many :test_drive_package_choices, :class_name => "JamRuby::TestDrivePackageChoice", :foreign_key => "user_id", inverse_of: :user belongs_to :desired_package, :class_name => "JamRuby::LessonPackageType", :foreign_key => "lesson_package_type_id", inverse_of: :user_desired_packages # used to hold whether user last wanted test drive 4/2/1 - + belongs_to :lesson_package_needs_purchase, :class_name => "JamRuby::LessonPackageType", :foreign_key => "lesson_package_needs_purchase_id", inverse_of: :users_needing_purchase # Shopping carts has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" @@ -477,7 +477,7 @@ module JamRuby def age now = Time.now.utc.to_date - self.birth_date.nil? ? "" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) + self.birth_date.nil? ? nil : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) end def session_count @@ -685,6 +685,8 @@ module JamRuby id end + + def set_password(old_password, new_password, new_password_confirmation) # so that UserObserver knows to send a confirmation email on success @@ -720,7 +722,7 @@ module JamRuby end def change_password(new_password, new_password_confirmation) - # FIXME: Should verify that the new password meets certain quality criteria. Really, maybe that should be a + # FIXME: Should verify that the new password meets certain quality criteria. Really, maybe that should be a # verification step. self.updating_password = true self.password = new_password @@ -1358,17 +1360,21 @@ 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 - # first try posa card posa_card = PosaCard.where(code: gift_card).first if posa_card posa_card.claim(user) user.posa_cards << posa_card + user.purchase_required = posa_card.requires_purchase # temporary; just so the signup page knows to send them to payment place 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 + if found_gift_card + user.gift_cards << found_gift_card + end + end end @@ -1448,6 +1454,17 @@ module JamRuby #if school && school.education # UserMailer.student_education_welcome_message(user).deliver_now #else + body = "Name: #{user.name}\n" + body << "Email: #{user.email}\n" + body << "Admin: #{user.admin_student_url}\n" + if posa_card + body << "Package Details: \n" + body << " Package: #{posa_card.lesson_package_type.id}\n" + body << " Credits: #{posa_card.credits}\n" + body << " Code: #{posa_card.code}\n" + end + + AdminMailer.jamclass_alerts({subject: "#{user.name} just signed up as a student", body: body}).deliver_now UserMailer.student_welcome_message(user).deliver_now #end elsif user.is_a_teacher @@ -1975,6 +1992,10 @@ module JamRuby APP_CONFIG.admin_root_url + "/admin/users/" + id end + def admin_student_url + APP_CONFIG.admin_root_url + "/admin/students" # should add id; not yet supported + end + def jam_track_rights_admin_url APP_CONFIG.admin_root_url + "/admin/jam_track_rights?q[user_id_equals]=#{id}&commit=Filter&order=created_at DESC" end @@ -2042,7 +2063,7 @@ module JamRuby # 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 + posa_cards.where('is_lesson = ?', true).where('claimed_at > ?', APP_CONFIG.jam_class_card_wait_period_year.years.ago).count == 0 end def lessons_with_teacher(teacher) @@ -2197,6 +2218,7 @@ module JamRuby lesson_package_type = nil uncollectables = nil choice = nil + posa_card = nil User.transaction do if params[:name].present? @@ -2209,20 +2231,29 @@ module JamRuby if params[:test_drive] self.reload if booking + # bookin will indicate test package lesson_package_type = booking.resolved_test_drive_package + posa_card = booking.posa_card elsif choice + # packages also indicate lesson package lesson_package_type = choice.lesson_package_type + elsif self.lesson_package_needs_purchase + # user has a POSA card requiring purchase, so this takes preference over the 'desired_package' (a user could have both set, but we force user to pay for POSA_CARD requiring purchase before picking up a random TD purchase) + lesson_package_type = self.lesson_package_needs_purchase + # also update POSA cards indicating they have been bought. This below code is a little bit + posa_card = self.posa_cards.where(requires_purchase: true).where(purchased:false).order(:created_at).first + else + # the user has at some point b4 indicated interest in a package; so in absence of above indicators, this is what they are buying + lesson_package_type = self.desired_package end - if lesson_package_type.nil? - lesson_package_type = LessonPackageType.test_drive_4 - end - - - result = Sale.purchase_test_drive(self, lesson_package_type, booking) + result = Sale.purchase_test_drive(self, lesson_package_type, booking, posa_card) test_drive = result[:sale] purchase = result[:purchase] + if posa_card && !purchase.errors.any? + posa_card.has_been_purchased(false) + end if booking && !purchase.errors.any? # the booking would not have a lesson_package_purchase associated yet, so let's associate it booking.lesson_sessions.update_all(lesson_package_purchase_id: purchase.id) @@ -2233,6 +2264,7 @@ module JamRuby end + {lesson: booking, test_drive: test_drive, purchase: purchase, lesson_package_type: lesson_package_type, uncollectables: uncollectables, package: choice} end @@ -2256,6 +2288,10 @@ module JamRuby 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_posa_card + posa_cards.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).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 diff --git a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb index d9097d3c6..c603a1f51 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb @@ -12,6 +12,7 @@ module JamRuby LessonSession.hourly_check TeacherPayment.hourly_check + @@log.debug("done") end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index e2ed04d33..15e948589 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -901,7 +901,24 @@ FactoryGirl.define do factory :posa_card, class: 'JamRuby::PosaCard' do sequence(:code) { |n| n.to_s } - card_type JamRuby::PosaCardType::JAM_TRACKS_5 + card_type JamRuby::PosaCard::JAM_TRACKS_5 + credits 5 + requires_purchase false + purchased true + + factory :posa_card_lesson_2 do + card_type JamRuby::PosaCard::JAM_CLASS_2 + credits 2 + lesson_package_type { JamRuby::LessonPackageType.test_drive_2 } + is_lesson true + end + + factory :posa_card_lesson_4 do + card_type JamRuby::PosaCard::JAM_CLASS_4 + credits 4 + lesson_package_type { JamRuby::LessonPackageType.test_drive_4 } + is_lesson true + end end factory :posa_card_type, class: 'JamRuby::PosaCardType' do diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb index f231f9d38..4534f3e2d 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -241,6 +241,47 @@ describe "Monthly Recurring Lesson Flow" do user.reload user.remaining_test_drives.should eql 0 UserMailer.deliveries.length.should eql 0 # one for student + + booking.remaining_roll_forward_amount_in_cents.should eql 0 + lesson_purchase.remaining_roll_forward_amount_in_cents.should be_nil + lesson_purchase.expected_session_times.should eql 2 + lesson_purchase.actual_session_times.should be_nil + + Timecop.travel(Date.new(2016, 4, 1)) + LessonBooking.adjust_for_missed_sessions + # we should find remaining/totals in lesson purcahse as well as booking rolled up + booking.reload + lesson_purchase.reload + lesson_purchase.actual_session_times.should eql 1 + lesson_purchase.expected_session_times.should eql 2 + booking.remaining_roll_forward_amount_in_cents.should eql (lesson_purchase.price * 100 * (1.0/2.0)).round + lesson_purchase.remaining_roll_forward_amount_in_cents.should eql (lesson_purchase.price * 100 * (1.0/2.0)).round + lesson_purchase.total_roll_forward_amount_in_cents.should eql (lesson_purchase.price * 100 * (1.0/2.0)).round + + # but once we bill out, we'll not credit as heavily + LessonBooking.bill_monthlies + booking.reload + lesson_purchase.reload + lesson_purchase.actual_session_times.should eql 1 + lesson_purchase.expected_session_times.should eql 2 + booking.remaining_roll_forward_amount_in_cents.should eql 0 + lesson_purchase.remaining_roll_forward_amount_in_cents.should eql 0 + lesson_purchase.total_roll_forward_amount_in_cents.should eql (lesson_purchase.price * 100 * (1.0/2.0)).round + + + booking.lesson_package_purchases.count.should eql 2 + next_purchase = booking.lesson_package_purchases.order(:created_at)[1] + next_purchase.remaining_roll_forward_amount_in_cents.should be nil + next_purchase.total_roll_forward_amount_in_cents.should be nil + next_purchase.expected_session_times.should eql 4 + next_purchase.actual_session_times.should be_nil + + next_purchase.teacher_distributions.count.should eql 1 + distribution = next_purchase.teacher_distributions[0] + distribution.amount_in_cents.should eql (3000 - 750) # booked price is 30. one lesson is 7.50 + distribution.teacher_fee_in_cents.should eql ((3000 - 750) * 0.28).round # booked price is 30. one lesson is 7.50. take out .28 + distribution.reduced_roll_forward_amount_in_cents.should eql 750 + next_purchase.reduced_roll_forward_amount_in_cents.should eql 750 end it "works (school on school education)" do diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb index 10c2e30fa..cfd1dc8d7 100644 --- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -16,7 +16,7 @@ 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(:card_lessons) {FactoryGirl.create(:posa_card_lesson_4)} let(:retailer) {FactoryGirl.create(:retailer)} before { @@ -271,6 +271,7 @@ describe "TestDrive Lesson Flow" do booking.user.should eql user booking.sent_notices.should be_true booking.posa_card.should eql card_lessons + booking.posa_card_purchased.should be_true user.unprocessed_test_drive.should be_nil teacher_user.has_booked_test_drive_with_student?(user).should be_true diff --git a/ruby/spec/jam_ruby/models/lesson_package_type_spec.rb b/ruby/spec/jam_ruby/models/lesson_package_type_spec.rb new file mode 100644 index 000000000..7ed389500 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_package_type_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe LessonPackageType do + + it "test drive packages" do + # 3 builtt-in, 3 amazon + LessonPackageType.test_drive_package_ids.count.should eql 6 + end + + describe "amazon price" do + it "4-count" do + LessonPackageType.amazon_test_drive_free_4.price.should eql 0 + end + + it "2-count" do + LessonPackageType.amazon_test_drive_free_2.price.should eql 0 + end + + it "4-count reduced" do + LessonPackageType.amazon_test_drive_paid_4.price.should eql 19.99 + end + + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb index 0f254d5e7..703d6b529 100644 --- a/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb @@ -26,7 +26,9 @@ describe LessonSessionMonthlyPrice do booking.booked_price.should eql 30.00 - booking.booked_price.should eql(LessonSessionMonthlyPrice.price(booking, jan1)) + price, times = LessonSessionMonthlyPrice.price(booking, jan1) + booking.booked_price.should eql(price) + times.should eql(5) end it "middle of the month" do @@ -38,7 +40,10 @@ describe LessonSessionMonthlyPrice do booking.booked_price.should eql 30.00 - ((booking.booked_price * 0.75).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + price, times = LessonSessionMonthlyPrice.price(booking, jan17) + ((booking.booked_price * 0.75).round(2)).should eql(price) + times.should eql(3) + end it "end of the month which has a last billable day based on slot" do @@ -50,7 +55,9 @@ describe LessonSessionMonthlyPrice do booking.booked_price.should eql 30.00 - ((booking.booked_price * 0.25).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + price, times = LessonSessionMonthlyPrice.price(booking, jan17) + ((booking.booked_price * 0.25).round(2)).should eql(price) + times.should eql(1) end it "end of the month which is not a last billable days" do @@ -62,7 +69,9 @@ describe LessonSessionMonthlyPrice do booking.booked_price.should eql 30.00 - ((booking.booked_price * 0.0).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, feb29)) + price, times = LessonSessionMonthlyPrice.price(booking, feb29) + ((booking.booked_price * 0.0).round(2)).should eql(price) + times.should eql(0) end end end diff --git a/ruby/spec/jam_ruby/models/posa_card_spec.rb b/ruby/spec/jam_ruby/models/posa_card_spec.rb index cfa886a9c..147101851 100644 --- a/ruby/spec/jam_ruby/models/posa_card_spec.rb +++ b/ruby/spec/jam_ruby/models/posa_card_spec.rb @@ -5,7 +5,8 @@ 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(:card_lessons) {FactoryGirl.create(:posa_card_lesson_4)} + let(:card_lessons_2) {FactoryGirl.create(:posa_card_lesson_2)} let(:retailer) {FactoryGirl.create(:retailer)} it "created by factory" do card.touch @@ -42,10 +43,12 @@ describe PosaCard do end describe "claim" do - it "succeeds" do + it "succeeds simple" do PosaCard.activate(card, retailer) card.reload card.claim(user) + user.reload + user.jamclass_credits.should be 0 card.errors.any?.should be false card.claimed_at.should_not be_nil @@ -56,16 +59,33 @@ describe PosaCard do PosaCard.activate(card_lessons, retailer) card_lessons.reload card_lessons.claim(user) + user.jamclass_credits.should be 4 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.lesson_package_type.should eql card_lessons.lesson_package_type card_lessons.lesson_package_purchase.posa_card.should eql card_lessons end + it "succeeds with jamclass type 2-count" do + PosaCard.activate(card_lessons_2, retailer) + card_lessons_2.reload + card_lessons_2.claim(user) + user.reload + user.jamclass_credits.should be 2 + + card_lessons_2.errors.any?.should be false + card_lessons_2.claimed_at.should_not be_nil + card_lessons_2.user.should eql user + card_lessons_2.reload + card_lessons_2.lesson_package_purchase.should_not be_nil + card_lessons_2.lesson_package_purchase.lesson_package_type.should eql card_lessons_2.lesson_package_type + card_lessons_2.lesson_package_purchase.posa_card.should eql card_lessons_2 + end + it "associates student automatically for GC" do gc = GuitarCenter.init gc_owner = gc[:user] @@ -83,7 +103,7 @@ describe PosaCard do 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.lesson_package_type.should eql card_lessons.lesson_package_type card_lessons.lesson_package_purchase.posa_card.should eql card_lessons end @@ -120,16 +140,16 @@ describe PosaCard do end it "can't be within one year" do - PosaCard.activate(card, retailer) - card.reload - card.claim(user) + PosaCard.activate(card_lessons, retailer) + card_lessons.reload + card_lessons.claim(user) - PosaCard.activate(card2, retailer) - card2.reload - card2.claim(user) + PosaCard.activate(card_lessons_2, retailer) + card_lessons_2.reload + card_lessons_2.claim(user) - card2.errors.any?.should be true - card2.errors[:user].should eql ['was within 1 year'] + card_lessons_2.errors.any?.should be true + card_lessons_2.errors[:claimed_at].should eql ['was within 1 year'] end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index adcd5a147..b01b773b9 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -889,6 +889,7 @@ describe Sale do end it "can succeed with no booking; just intent" do + user.desired_package = LessonPackageType.test_drive_4 intent = TeacherIntent.create(user, teacher, 'book-test-drive') token = create_stripe_token result = user.payment_update({token: token, zip: '78759', test_drive: true}) @@ -912,6 +913,7 @@ describe Sale do end it "will reject second test drive purchase" do + user.desired_package = LessonPackageType.test_drive_4 intent = TeacherIntent.create(user, teacher, 'book-test-drive') token = create_stripe_token result = user.payment_update({token: token, zip: '78759', test_drive: true}) diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index c74e78f49..3d3bfe62f 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -710,7 +710,7 @@ describe User do user.age.should == 9 user.birth_date = nil - user.age.should == "" + user.age.should == nil end end @@ -730,7 +730,7 @@ describe User do end - it "allow no_show aggregation" do + it "allow no_show aggr egation" do user.mod_merge({"no_show" => {"some_screen1" => true}}) user.save! user.reload diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 01ce103cb..1b195b0f1 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -19,6 +19,10 @@ def app_config 'alerts@jamkazam.com' end + def email_jamclass_alerts_alias + 'jamclass-alerts@jamkazam.com' + end + def email_crashes_alias 'clientcrash@jamkazam.com' end diff --git a/web/app/assets/javascripts/dialog/shutdownDialog.js b/web/app/assets/javascripts/dialog/shutdownDialog.js index 061dd8017..5390f224d 100644 --- a/web/app/assets/javascripts/dialog/shutdownDialog.js +++ b/web/app/assets/javascripts/dialog/shutdownDialog.js @@ -11,6 +11,10 @@ context.JK.Banner.showAlert( { title: "Close JamKazam Application", buttons: [ + {name: 'CANCEL SHUTDOWN', click: function() { + logger.debug("'CANCEL SHUTDOWN' selected") + context.JK.Banner.hide(); + }}, {name: 'COMPLETELY SHUT DOWN THE APP', click: function() { logger.debug("'COMPLETELY SHUT DOWN THE APP' selected") context.jamClient.ShutdownApplication() diff --git a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee index 37f903af4..afc89d3a1 100644 --- a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee @@ -100,7 +100,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 && !response.teacher.school.education - @setState({teacher: response, isSelf: response.id == context.JK.currentUserId, school_on_school: school_on_school}) + @setState({teacher: response, isSelf: response.id == context.JK.currentUserId}) else logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId) @@ -235,7 +235,7 @@ UserStore = context.UserStore @setState({updating: false}) UserActions.refresh() #if response.user['has_stored_credit_card?'] || @state.school_on_school || response.posa_card_id? - if response.user['has_stored_credit_card?'] || response.posa_card_id? + if response.user['has_stored_credit_card?'] || (response.posa_card_id? && response.posa_card_purchased) context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.

We've taken you back to the JamClass home page, where you can check the status of this lesson, as well as any other past and future lessons.") url = "/client#/jamclass/lesson-booking/#{response.id}" url = "/client#/jamclass" @@ -302,16 +302,17 @@ UserStore = context.UserStore else for minutes in enabledMinutes lesson_price = teacher["price_per_lesson_#{minutes}_cents"] - value = "single|#{minutes}" - display = "#{minutes} Minute Lesson Each Week - $#{(lesson_price / 100).toFixed(2)} Per Week" - results.push(``) - + if lesson_price + value = "single|#{minutes}" + display = "#{minutes} Minute Lesson Each Week - $#{(lesson_price / 100).toFixed(2)} Per Week" + results.push(``) for minutes in enabledMinutes monthly_price = teacher["price_per_month_#{minutes}_cents"] - value = "monthly|#{minutes}" - display = "#{minutes} Minute Lesson Each Week - $#{(monthly_price / 100).toFixed(2)} Per Month" - results.push(``) + if monthly_price + value = "monthly|#{minutes}" + display = "#{minutes} Minute Lesson Each Week - $#{(monthly_price / 100).toFixed(2)} Per Month" + results.push(``) if results.length == 0 results.push(``) @@ -348,8 +349,8 @@ UserStore = context.UserStore am_pm = [``, ``] - bookLessonClasses = classNames({"button-orange": true, 'book-lesson-btn': true, disabled: !this.state.teacher? && !@state.updating}) - cancelClasses = classNames({"button-grey": true, disabled: !this.state.teacher? && !@state.updating}) + bookLessonClasses = classNames({"button-orange": true, 'book-lesson-btn': true, disabled: !this.state.teacher? || @state.updating}) + cancelClasses = classNames({"button-grey": true, disabled: !this.state.teacher? || @state.updating}) descriptionErrors = context.JK.reactSingleFieldErrors('description', @state.descriptionErrors) bookedPriceErrors = context.JK.reactSingleFieldErrors('booked_price', @state.bookedPriceErrors) diff --git a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee index a3062a088..08f901183 100644 --- a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee @@ -233,6 +233,11 @@ LessonTimerActions = context.LessonTimerActions rescheduleLesson: (lesson) -> if lesson.recurring buttons = [] + buttons.push({ + name: 'CANCEL', + buttonStyle: 'button-grey', + click: (() => (logger.debug("cancelling out of reschedule dialog"))) + }) buttons.push({ name: 'THIS LESSON', buttonStyle: 'button-orange', diff --git a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee index 2f70c9221..e58ec7a77 100644 --- a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee @@ -39,7 +39,7 @@ UserStore = context.UserStore return slot.slotTime = @slotTime(slot, booking) - slot.is_recurring = @isRecurring() + slot.is_recurring = slot.slot_type == 'recurring' if slot['is_teacher_created?'] slot.creatorRole = 'teacher' slot.creatorRoleRelative = 'teacher' @@ -169,6 +169,7 @@ UserStore = context.UserStore @focusedLesson()? focusedLesson: () -> + console.log("focusedlesos", this.state?.booking?.focused_lesson) this.state?.booking?.focused_lesson updateBookingState: (booking) -> @@ -425,10 +426,22 @@ UserStore = context.UserStore false isApproved: () -> - @state.booking?.status == 'approved' + if @hasFocusedLesson() + @focusedLesson().status == 'approved' + else + @state.booking?.status == 'approved' isCanceled: () -> - @state.booking?.status == 'canceled' + + cancelled = @state.booking?.status == 'canceled' + if cancelled + # if the booking is canceelled, lessons are done too. No need to check the focused lesson + return true + else + if @hasFocusedLesson() + return @focusedLesson().status == 'canceled' + else + cancelled isSuspended: () -> @state.booking?.status == 'suspended' @@ -498,13 +511,15 @@ UserStore = context.UserStore text = "Preferred day/time for lesson is #{this.slotTime(defaultSlot)}. Secondary option is #{this.slotTime(altSlot)}." slotTime: (slot, booking = this.state.booking) -> - if @hasFocusedLesson() || !@isRecurring(booking) + if @hasFocusedLesson() || slot.slot_type == 'single' #!@isRecurring(booking) slot.pretty_start_time else "#{this.dayOfWeek(slot)} at #{this.dayTime(slot)}" slotTimePhrase: (slot) -> - if @isRecurring() + console.log("SLOT", slot) + if slot.slot_type == 'recurring' + # if @isRecurring() "each " + @slotTime(slot) else @slotTime(slot) @@ -709,6 +724,10 @@ UserStore = context.UserStore header = 'respond to lesson request' content = @renderTeacherRequested() + else if @isApproved() + header = @approvedHeader() + content = @renderTeacherApproved() + else if @isCounter() if @isTeacherCountered() header = 'your proposed alternate day/time is still pending' @@ -716,10 +735,6 @@ UserStore = context.UserStore header = 'student has proposed an alternate day/time' content = @renderTeacherCountered() - else if @isApproved() - header = @approvedHeader() - content = @renderTeacherApproved() - else if @isCompleted() header = @completedHeader() content = @renderTeacherComplete() @@ -750,6 +765,11 @@ UserStore = context.UserStore header = 'your lesson has been requested' content = @renderStudentRequested() + else if @isApproved() + header = @approvedHeader() + content = @renderStudentApproved() + + else if @isCounter() if @isTeacherCountered() header = 'teacher has proposed an alternate day/time' @@ -757,10 +777,6 @@ UserStore = context.UserStore header = 'your proposed alternate day/time is still pending' content = @renderTeacherCountered() - else if @isApproved() - header = @approvedHeader() - content = @renderStudentApproved() - else if @isCompleted() header = @completedHeader() content = @renderStudentComplete() diff --git a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee index b63955402..697ff2d77 100644 --- a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee @@ -251,7 +251,7 @@ proposeAltLabelText = "Propose alternate day/time" for slot, i in @props.slots - if this.props.is_recurring + if slot.is_recurring slotDetail = `
Each {slot.slotTime}
` else slotDetail = `
{slot.slotTime}
` diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee index f8ba02b5d..326660e43 100644 --- a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -307,47 +307,46 @@ UserStore = context.UserStore if response.test_drive? # ok, they bought a package if response.lesson_package_type? - # always of form test-drive-# - prefixLength = "test-drive-".length - packageLength = response.lesson_package_type.package_type.length - testDriveCount = response.lesson_package_type.package_type.substring(prefixLength, packageLength) + testDriveCount = response.lesson_package_type.credits logger.debug("testDriveCount: " + testDriveCount) - testDriveCountInt = parseInt(testDriveCount); - if context._.isNaN(testDriveCountInt) - testDriveCountInt = 3 + context.JK.GA.trackTestDrivePurchase(testDriveCount); - context.JK.GA.trackTestDrivePurchase(testDriveCountInt); - - if response.test_drive?.teacher_id - teacher_id = response.test_drive.teacher_id - if testDriveCount == '1' + if response.package? + logger.debug("packaged TD purchase...") + # the user bought a package that already includes the 1, 2, or 4 teachers as part of it + if testDriveCount == 1 text = "You have purchased 1 TestDrive credit and have used it to request a JamClass with #{@state.package.teachers[0].user.name}. The teacher has received your request and should respond shortly." - else if response.package? + location = "/client#/jamclass" + else text = "Each teacher has received your request and should respond shortly." - else - text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly." + location = "/client#/jamclass" + else if response.lesson? + logger.debug("TD with a lesson booking...") + # there is a lesson booking in context, so the user has gotten a credit + gotten a lesson request going forward + text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit to request a JamClass with #{@state.teacher?.name}. The teacher has received your request and should respond shortly." location = "/client#/jamclass" - else - if @state.teacher?.id - - # the user bought the testdrive, and there is a teacher of interest in context (but no booking) - if testDriveCount == '1' - text = "You now have 1 TestDrive credit.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." - location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id - else - text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." - location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id + else if response.teacher?.id + logger.debug("straight TD package + teacher interest") + # the user bought a test drive straight up with no lesson-booking, but previously showed interest in a teacher + teacher_id = response.teacher.id + if testDriveCount == '1' + text = "You now have 1 TestDrive credit.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." + location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id else - # the user bought test drive, but 'cold' , i.e., no teacher in context - if testDriveCount == '1' - text = "You now have 1 TestDrive credit.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." - location = "/client#/teachers/search" - else - text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." - location = "/client#/teachers/search" + text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the lesson booking screen for the teacher you initially showed interest in." + location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id + else + logger.debug("straight TD package") + # the user bought test drive, but 'cold' , and, no teacher in context + if testDriveCount == 1 + text = "You now have 1 TestDrive credit.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." + location = "/client#/teachers/search" + else + text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.

We've taken you to the Teacher Search screen, so you can search for teachers right for you." + location = "/client#/teachers/search" context.JK.Banner.showNotice("TestDrive Purchased",text) window.location = location @@ -495,14 +494,24 @@ UserStore = context.UserStore else alert("unknown package type") else - if this.state.user.lesson_package_type_id == 'test-drive' + # the UI is currenty the same whether the user is browsing around indicating what type of TD they want, or + # if they bought a POSA card required puchase, and have the this.state.user.lesson_package_needs_purchase_id + # we just need to figure out which is set, giving preference to the POSA requiring purchase + lesson_package_type_id = this.state.user.lesson_package_needs_purchase_id + if !lesson_package_type_id? + lesson_package_type_id = this.state.user.lesson_package_type_id + + if lesson_package_type_id == 'test-drive' explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 4 private online music lessons - 1 each from 4 different instructors in the JamClass instructor community. The price of this TestDrive package is $49.99.` - else if this.state.user.lesson_package_type_id == 'test-drive-1' + else if lesson_package_type_id == 'test-drive-1' explanation =`You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 1 private online music lesson from an instructor in the JamClass instructor community. The price of this TestDrive package is $14.99.` - else if this.state.user.lesson_package_type_id == 'test-drive-2' + else if lesson_package_type_id == 'test-drive-2' explanation =`You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.` + else if lesson_package_type_id == 'amazon-test-drive-paid-4' + explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 4 private online music lessons - 1 each from 4 different instructors in the JamClass instructor community. The price of this TestDrive package is $19.99.` else - alert("You do not have a test drive package selected: " + this.state.user.lesson_package_type_id ) + explanation = `Loading your TestDrive packaging info...` + #alert("You do not have a test drive package selected: " + this.state.user.lesson_package_type_id ) bookingDetail = `

{explanation} @@ -531,7 +540,7 @@ UserStore = context.UserStore

` else if this.state.lesson.payment_style == 'weekly' bookingInfo = `

You are booking a weekly recurring series of {lesson_length}-minute - lessons, to be paid individually as each lesson is taken, until cancelled.

` + lessons for ${this.bookedPrice()}, to be paid individually as each lesson is taken, until cancelled.

` bookingDetail = `

Your card will be charged on the day of each lesson. If you need to cancel a lesson, you must do so at least 24 hours before the lesson is scheduled, or you will be charged for the lesson in full. @@ -542,7 +551,7 @@ UserStore = context.UserStore

` else if this.state.lesson.payment_style == 'monthly' bookingInfo = `

You are booking a weekly recurring series of {lesson_length}-minute - lessons, to be paid for monthly until cancelled.

` + lessons for ${this.bookedPrice()}, to be paid for monthly until cancelled.

` bookingDetail = `

Your card will be charged on the first day of each month. Canceling individual lessons does not earn a refund when buying monthly. To cancel, you must cancel at least 24 hours before the beginning of the diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee index 43d0a0902..900c737fd 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -81,7 +81,7 @@ proficiencyDescriptionMap = { # mount it @profileClipboard = new Clipboard($profileLink.get(0), { text: => - return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.teacher?.id) + return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.id) }) else if $profileLink.length == 0 && @profileClipboard? @profileClipboard.destroy() @@ -245,10 +245,15 @@ proficiencyDescriptionMap = { biography = biography.replace(/\n/g, "
") - `

- + # make button available only if this is the current user viewing + if @state.userId == @user?.id + + copyUrlToClipboard = ` COPY PROFILE URL TO CLIPBOARD -

Teacher Profile {this.editProfileLink('edit profile', 'introduction')}

+ ` + + `
+ {copyUrlToClipboard}

Teacher Profile {this.editProfileLink('edit profile', 'introduction')}

@@ -696,6 +701,17 @@ proficiencyDescriptionMap = { else age = null + backgroundCheck = null + backgroundCheckAt = @state.user?.teacher?.background_check_at + + if backgroundCheckAt + backgroundCheck = + `
+
Background Check:
+ +
{context.JK.formatDateShort(new Date(backgroundCheckAt))}
+
` + `
@@ -709,6 +725,8 @@ proficiencyDescriptionMap = {
Last Signed In:
{"very recently"}
+ + {backgroundCheck}
` diff --git a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee index c15e12db1..2a2e2fea4 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -137,10 +137,10 @@ ProfileActions = @ProfileActions if @state.next == null @contentBodyScroller.off('scroll') - if @state.currentPage == 1 and @state.results.length == 0 + if @state.currentPage == 1 and @state.results.length == 0 && !@state.searching @endOfList.text('No Teachers found matching your search').show() logger.debug("TeacherSearch: empty search") - else if @state.currentPage > 0 + else if @state.currentPage > 0 && !@state.searching logger.debug("end of search") @endOfList.text('No more Teachers').show() else @@ -238,13 +238,16 @@ ProfileActions = @ProfileActions bookSingleBtn = null bookTestDriveBtn = null - + backgroundCheck = null + if user.teacher.background_check_at + backgroundCheck = `` if !school_on_school && (!@state.user? || @state.user.jamclass_credits > 0 || @state.user.remaining_test_drives > 0 || @state.user['can_buy_test_drive?']) bookTestDriveBtn = `BOOK TESTDRIVE LESSON` else bookSingleBtn = `BOOK LESSON` resultsJsx.push(`
+ {backgroundCheck}
diff --git a/web/app/assets/javascripts/react-components/landing/AccountActivatePage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/AccountActivatePage.js.jsx.coffee new file mode 100644 index 000000000..04f3456d4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/AccountActivatePage.js.jsx.coffee @@ -0,0 +1,200 @@ +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.' +@AccountActivatePage = React.createClass({ + + render: () -> + + + if this.state.formErrors? + for key, value of this.state.formErrors + break + + errorText = context.JK.getFullFirstError(key, @state.formErrors, {email: 'Email', password: 'Password', gift_card: 'Coupon Code', posa_cards: 'Coupon Code', 'terms_of_service' : 'The terms of service', claimed_at: "Last coupon code used"}) + + 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 + if this.state.gifted_jamtracks + + button = + `
+
You now have {this.state.gifted_jamtracks} JamTracks credits on your account!
+ +
` + else + if this.state.purchase_required + button = + `
+
You have claimed {this.state.jamclass_credits} 30-minute JamClass credits, but still need to enter your credit card info!
+ +
` + else + button = + `
+
You now have {this.state.jamclass_credits} 30-minute JamClass credits on your account!
+ +
` + else + if context.JK.currentUserId? + if @state.processing + buttonText = 'ACTIVATING COUPON CODE...' + else + buttonText = 'ACTIVATE COUPON CODE' + else + if @state.processing + buttonText = 'ACTIVATING ACCOUNT...' + else + buttonText = 'ACTIVATE ACCOUNT' + + button = `` + + action = ` + {button} + ` + + title = 'Activate Account' + + if context.JK.currentUserId? + form = + `
+ + {action} +
` + instruments = `

Enter the 10-digit code you received in your email and click the Activate button below.

` + else + form = + `
+ + + +
+ +
+ {action} + ` + instruments = `

Enter the 10-digit code you received in your email and click the Activate button below.

` + + + classes = classNames({'redeem-container': true, 'not-logged-in': !context.JK.currentUserId?, 'logged-in': context.JK.currentUserId? }) + `
+
+

{title}

+ {instruments} + {form} +
+ {errorText} +
+
+
` + + getInitialState: () -> + {formErrors: null, processing:false, gifted_jamtracks: null, jamclass_credits: null, purchase_required: null} + + 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') + console.log("$checkbox", $checkbox) + context.JK.checkbox($checkbox) + + submit: (e) -> + @action(e) + action: (e) -> + + if @state.done || @state.processing + e.preventDefault() + return + + if context.JK.currentUserId? + @redeem(e) + else + @signup(e) + + redeem: (e) -> + e.preventDefault() + return if @state.done || @state.processing + + $root = $(@getDOMNode()) + $code = $root.find('input[name="code"]') + code = $code.val() + + + @setState({processing:true}) + + rest.redeemGiftCard({gift_card: code}) + .done((response) => + + @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, jamclass_credits: response.jamclass_credits, purchase_required: response.purchase_required}) + + ).fail((jqXHR) => + @setState({processing:false}) + + if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + if response.errors + @setState({formErrors: response.errors}) + else + context.JK.app.notify({title: 'Unknown Error', text: jqXHR.responseText}) + else + context.JK.app.notifyServerError(jqXHR, "Unable to Apply Coupon") + ) + + signup: (e) -> + e.preventDefault() + + return if @state.done || @state.processing + + $root = $(@getDOMNode()) + $email = $root.find('input[name="email"]') + $code = $root.find('input[name="code"]') + $password = $root.find('input[name="password"]') + terms = $root.find('input[name="terms"]').is(':checked') + + @setState({processing:true}) + email = $email.val() + password = $password.val() + code = $code.val() + if !code + # must pass up non-null value to indicate user is trying to redeem giftcard while creating account + code = '' + + 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, jamclass_credits: response.jamclass_credits, purchase_required: response.purchase_required}) + + ).fail((jqXHR) => + @setState({processing:false}) + + if jqXHR.status == 422 + response = JSON.parse(jqXHR.responseText) + if response.errors + @setState({formErrors: response.errors}) + else + context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText}) + else + context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up") + ) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee index cfb102bc1..9d9d5c1f3 100644 --- a/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/RedeemGiftCardPage.js.jsx.coffee @@ -35,11 +35,18 @@ badCode = 'This is not a valid code. Please carefully re-enter the code and try
` else - button = - `
-
You now have {this.state.gifted_jamclass}, 30-minute JamClass credits on your account!
- -
` + if this.state.purchase_required + button = + `
+
You have claimed {this.state.gifted_jamclass} 30-minute JamClass credits, but still need to enter your credit card info!
+ +
` + else + button = + `
+
You now have {this.state.gifted_jamclass}, 30-minute JamClass credits on your account!
+ +
` else button = `` @@ -66,7 +73,7 @@ badCode = 'This is not a valid code. Please carefully re-enter the code and try
{action} ` - instruments = `

“Enter the 10-digit code from the back of your gift card and click the Redeem button below.

` + instruments = `

Enter the 10-digit code from the back of your gift card and click the Redeem button below.

` classes = classNames({'redeem-container': true, 'not-logged-in': !context.JK.currentUserId?, 'logged-in': context.JK.currentUserId? }) @@ -121,10 +128,13 @@ badCode = 'This is not a valid code. Please carefully re-enter the code and try $code = $root.find('input[name="code"]') code = $code.val() + + @setState({processing:true}) + rest.redeemGiftCard({gift_card: code}) .done((response) => - @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass}) + @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass, purchase_required: response.purchase_required}) ).fail((jqXHR) => @setState({processing:false}) @@ -161,7 +171,7 @@ badCode = 'This is not a valid code. Please carefully re-enter the code and try 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, gifted_jamclass: response.gifted_jamclass}) + @setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass, purchase_required: response.purchase_required}) ).fail((jqXHR) => @setState({processing:false}) diff --git a/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee index c61027267..45d7354c8 100644 --- a/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee +++ b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee @@ -41,9 +41,9 @@ teacherActions = window.JK.Actions.Teacher else if lesson.status == 'canceled' lesson.displayStatus = 'Canceled' if lesson.student_canceled - lesson.displayStatus = 'Canceled (Student)' + lesson.displayStatus = 'Canceled (by Student)' else if lesson.teacher_canceled - lesson.displayStatus = 'Canceled (Teacher)' + lesson.displayStatus = 'Canceled (by Teacher)' else if lesson.status == 'suspended' lesson.displayStatus = 'Suspended' @@ -60,15 +60,15 @@ teacherActions = window.JK.Actions.Teacher if lesson.analysis?.teacher_analysis?.missed && lesson.analysis?.student_analysis?.missed lesson.missedRole = 'both student and teacher' lesson.missedUser = lesson.teacher - lesson.displayStatus = 'Missed (Both)' + lesson.displayStatus = 'Missed (by Both)' else if lesson.analysis?.teacher_analysis?.missed lesson.missedRole = 'the teacher' lesson.missedUser = lesson.teacher - lesson.displayStatus = 'Missed (Teacher)' + lesson.displayStatus = 'Missed (by Teacher)' else if lesson.analysis?.student_analysis?.missed lesson.missedRole = 'the student' lesson.missedUser = lesson.student - lesson.displayStatus = 'Missed (Student)' + lesson.displayStatus = 'Missed (by Student)' else lesson.displayStatus = 'Missed' diff --git a/web/app/assets/stylesheets/client/react-components/TeacherProfile.scss b/web/app/assets/stylesheets/client/react-components/TeacherProfile.scss index b1f2fbcec..2148a3454 100644 --- a/web/app/assets/stylesheets/client/react-components/TeacherProfile.scss +++ b/web/app/assets/stylesheets/client/react-components/TeacherProfile.scss @@ -62,6 +62,14 @@ .backgroundCheck { margin-top:20px; + img { + margin-left: 10px; + position: relative; + top: 4px; + } + .background-check-time { + margin-top:10px; + } } .introductory-video { diff --git a/web/app/assets/stylesheets/client/react-components/TeacherSearch.scss b/web/app/assets/stylesheets/client/react-components/TeacherSearch.scss index 9353e2d20..ee0b183d1 100644 --- a/web/app/assets/stylesheets/client/react-components/TeacherSearch.scss +++ b/web/app/assets/stylesheets/client/react-components/TeacherSearch.scss @@ -98,6 +98,11 @@ } } + .background-check { + position: absolute; + top: 34px; + left: 24px; + } .user-avatar { text-align:center; float:left; diff --git a/web/app/assets/stylesheets/landings/account_activate.scss b/web/app/assets/stylesheets/landings/account_activate.scss new file mode 100644 index 000000000..9afa413ed --- /dev/null +++ b/web/app/assets/stylesheets/landings/account_activate.scss @@ -0,0 +1,90 @@ +@import "client/common"; + +body.web.account_activate { + + h2 { + margin-bottom:20px; + } + + label{ + margin-bottom:4px; + color:$ColorTextTypical; + } + + input{ + margin-bottom:20px; + width:200px; + } + + .redeem-container { + margin-left:350px; + width:400px; + padding-top:20px; + + &.logged-in { + button { + margin-top:10px !important; + } + } + + &.not-logged-in { + + } + } + .redeem-content { + + } + p.instructions { + line-height:125%; + color:$ColorTextTypical; + margin-bottom:20px; + } + + button { + display:block !important; + height: 29px !important; + margin-bottom: 10px; + margin-right: 0px; + font-size: 16px !important; + padding: 7px 3px !important; + line-height:inherit !important; + margin-left:2px !important; + margin-top:15px; + } + + .icheckbox_minimal { + float: left; + top: -2px; + margin-left: 0; + margin-right:10px; + } + + .errors { + font-size:14px; + height:20px; + margin:0; + visibility: hidden; + color: red; + font-weight: bold; + + &.active { + visibility: visible; + } + } + + form { + margin-bottom:20px; + } + .terms-help { + float:left; + margin-top:-5px; + font-size:12px; + width:178px; + } + + .done-action { + margin-top: 20px; + line-height: 125%; + } + +} \ No newline at end of file diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index c63445f97..a4d255149 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -343,6 +343,35 @@ class ApiMusicSessionsController < ApiController end render :json => {}, :status => :ok end + + + begin + + lesson_link = nil + session_link = @history.music_session.admin_url + if @history.music_session.lesson_session + session_type = "Lesson" + lesson_link = @history.music_session.lesson_session.admin_url + else + session_type = "Session" + end + + + subject = "#{current_user.name} Rated Their #{session_type}!" + body = "Session Type: #{session_type}\n" + body << "Session Rating: #{@history.good_rating? ? "Good" : "Bad"}\n" + body << "User: #{current_user.email}\n" + body << "Music Session URL: #{session_link}\n" + if lesson_link + body << "Lesson URL: #{lesson_link}\n" + end + body << "Session Comments: #{@history.rating_comment}\n" + + AdminMailer.jamclass_alerts({subject: subject, body: body}).deliver_now + rescue Exception => e + logger.error("Exception sending out ratings email. Boo #{e}") + end + elsif request.get? render :json => { :should_rate_session => @history.should_rate_session? }, :status => :ok end diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 311d6cbbf..e0df0a195 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -25,9 +25,10 @@ class ApiUsersController < ApiController end def calendar - @user=lookup_user - ics = CalendarManager.new.create_ics_feed(@user) - send_data ics, :filename => 'JamKazam', :disposition => 'inline', :type => "text/calendar" + #@user=lookup_user + #ics = CalendarManager.new.create_ics_feed(@user) + #send_data ics, :filename => 'JamKazam', :disposition => 'inline', :type => "text/calendar" + render :json => {}, :status => 200 end def show @@ -1015,12 +1016,12 @@ class ApiUsersController < ApiController if @posa_card.errors.any? respond_with_model(@posa_card) else - if @posa_card.card_type == PosaCard::JAM_CLASS_4 - render json: {gifted_jamclass: 4}, status: 200 + if @posa_card.is_lesson_posa_card? + render json: {gifted_jamclass: @posa_card.credits}, status: 200 elsif @posa_card.card_type == PosaCard::JAM_TRACKS_10 - render json: {gifted_jamtracks: 10}, status: 200 + render json: {gifted_jamtracks: @posa_card.credits}, status: 200 elsif @posa_card.card_type == PosaCard::JAM_TRACKS_5 - render json: {gifted_jamtracks: 5}, status: 200 + render json: {gifted_jamtracks: @posa_card.credits}, status: 200 else raise 'unknown card_type ' + @posa_card.card_type end diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index d24a0c987..94e64728b 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -310,6 +310,12 @@ class LandingsController < ApplicationController render 'redeem_giftcard', layout: 'web' end + def account_activate + @no_landing_tag = true + @landing_tag_play_learn_earn = true + render 'account_activate', layout: 'web' + end + def buy_gift_card @no_landing_tag = true @landing_tag_play_learn_earn = true diff --git a/web/app/views/api_lesson_bookings/show.rabl b/web/app/views/api_lesson_bookings/show.rabl index 03e28423c..13e35bfe1 100644 --- a/web/app/views/api_lesson_bookings/show.rabl +++ b/web/app/views/api_lesson_bookings/show.rabl @@ -1,6 +1,6 @@ object @lesson_booking -attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message, :booked_price, :card_presumed_ok, :no_slots, :posa_card_id +attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message, :booked_price, :card_presumed_ok, :no_slots, :posa_card_id, :posa_card_purchased, :remaining_roll_forward_amount_in_cents child(:lesson_booking_slots => :slots) { attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone, :pretty_timezone, :from_package diff --git a/web/app/views/api_lesson_sessions/show.rabl b/web/app/views/api_lesson_sessions/show.rabl index abd49cafb..434ab54e9 100644 --- a/web/app/views/api_lesson_sessions/show.rabl +++ b/web/app/views/api_lesson_sessions/show.rabl @@ -4,7 +4,7 @@ object @lesson_session :status, :student_canceled, :teacher_canceled, :student_canceled_at, :teacher_canceled_at, :student_canceled_reason, :teacher_canceled_reason, :status, :success, :teacher_unread_messages, :student_unread_messages, :is_active?, :recurring, :analysed, :school_on_school?, :no_school_on_school_payment?, :payment_if_school_on_school?, :teacher_id, :student_id, :pretty_scheduled_start, :scheduled_start, :teacher_short_canceled, - :best_display_time + :best_display_time, :remaining_roll_forward_amount_in_cents node do |lesson_session| { diff --git a/web/app/views/api_stripe/store.rabl b/web/app/views/api_stripe/store.rabl index bbf5e8fcc..b9d07c0e7 100644 --- a/web/app/views/api_stripe/store.rabl +++ b/web/app/views/api_stripe/store.rabl @@ -1,15 +1,18 @@ object @lesson -if @lesson +if @lesson # is a LessonBooking node :lesson do |lesson| - {id: @lesson.id } + { + id: @lesson.id, + teacher_id: @lesson.teacher_id + } end end -if @test_drive +if @test_drive # is a Sale object node :test_drive do |lesson| - {teacher_id: @test_drive.id} + {id: @test_drive.id} end end @@ -29,7 +32,7 @@ end if @lesson_package_type node :lesson_package_type do |lesson_package_type| - {package_type: @lesson_package_type.package_type} + {package_type: @lesson_package_type.package_type, credits: @lesson_package_type.test_drive_count} end end diff --git a/web/app/views/api_teachers/detail.rabl b/web/app/views/api_teachers/detail.rabl index b60e8e1d8..3f0ca2f6e 100644 --- a/web/app/views/api_teachers/detail.rabl +++ b/web/app/views/api_teachers/detail.rabl @@ -36,7 +36,8 @@ attributes :id, :test_drives_per_week, :errors, :profile_pct, - :school_id + :school_id, + :background_check_at child :review_summary => :review_summary do diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index 99b7eea80..d077c0bea 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -34,7 +34,7 @@ end # give back more info if the user being fetched is yourself if current_user && @user == current_user - attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack, :remaining, :has_stored_credit_card?, :remaining_test_drives, :jamclass_credits, :can_buy_test_drive?, :lesson_package_type_id, :school_id, :is_guitar_center_student? + attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack, :remaining, :has_stored_credit_card?, :remaining_test_drives, :jamclass_credits, :can_buy_test_drive?, :lesson_package_type_id, :school_id, :is_guitar_center_student?, :purchase_required, :lesson_package_needs_purchase_id node :owned_school_id do |user| user.owned_school.id if user.owned_school diff --git a/web/app/views/api_users/show_teacher_index.rabl b/web/app/views/api_users/show_teacher_index.rabl index e96264eb4..8cf323f2c 100644 --- a/web/app/views/api_users/show_teacher_index.rabl +++ b/web/app/views/api_users/show_teacher_index.rabl @@ -4,7 +4,7 @@ object @user attributes :id, :first_name, :last_name, :name, :photo_url child :teacher do |teacher| - attributes :id, :biography, :school_id + attributes :id, :biography, :school_id, :background_check_at child :school do |school| attributes :id, :education diff --git a/web/app/views/landings/account_activate.html.slim b/web/app/views/landings/account_activate.html.slim new file mode 100644 index 000000000..2b3d2f217 --- /dev/null +++ b/web/app/views/landings/account_activate.html.slim @@ -0,0 +1,5 @@ +- provide(:page_name, 'landing_page full account_activate') +- provide(:description, 'Here you can redeem a code and associate it with your JamKazam account.') +- provide(:title, 'Activate Account') + += react_component 'AccountActivatePage' diff --git a/web/config/application.rb b/web/config/application.rb index f6f4162f6..8b300ace8 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -251,6 +251,7 @@ if defined?(Bundler) config.email_social_alias = 'social@jamkazam.com' config.email_crashes_alias = 'clientcrash@jamkazam.com' config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails + config.email_jamclass_alerts_alias= 'jamclass-alerts@jamkazam.com' config.email_generic_from = 'nobody@jamkazam.com' config.email_recurly_notice = 'recurly-alerts@jamkazam.com' config.email_smtp_address = 'smtp.sendgrid.net' diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index ffb0ecb12..99774e570 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -90,6 +90,7 @@ SampleApp::Application.configure do config.minimal_curtain = true config.video_available= ENV['VIDEO_AVAILABILITY'] || "full" config.email_generic_from = 'nobody-dev@jamkazam.com' + config.email_jamclass_alerts_alias= ENV['ALERT_EMAIL'] || 'jamclass-alerts-dev@jamkazam.com' config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com' config.email_crashes_alias = ENV['ALERT_EMAIL'] || 'clientcrash-dev@jamkazam.com' config.email_social_alias = ENV['ALERT_EMAIL'] || 'social-dev@jamkazam.com' diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb index 43cad1f72..f692a3813 100644 --- a/web/config/environments/test.rb +++ b/web/config/environments/test.rb @@ -53,7 +53,7 @@ SampleApp::Application.configure do config.websocket_gateway_enable = false config.websocket_gateway_port = 6759 - config.websocket_gateway_uri = "ws://localhost:#{config.websocket_gateway_port}/websocket" + config.websocket_gateway_uri = "ws://127.0.0.1:#{config.websocket_gateway_port}/websocket" #config.websocket_gateway_connect_time_stale_client = 4 #config.websocket_gateway_connect_time_expire_client = 6 diff --git a/web/config/routes.rb b/web/config/routes.rb index 514e228ee..521e3bae4 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -22,6 +22,7 @@ Rails.application.routes.draw do delete '/signout', to: 'sessions#destroy' match '/redeem_giftcard', to: 'landings#redeem_giftcard', via: :get + match '/account/activate/code', to: 'landings#account_activate', via: :get # landing pageslanding get '/jamtracks', to: 'landings#simple_jamtracks', as: 'landing_simple_jamtracks' diff --git a/web/lib/tasks/lesson.rake b/web/lib/tasks/lesson.rake index fc5f0180c..9fb3ad0e3 100644 --- a/web/lib/tasks/lesson.rake +++ b/web/lib/tasks/lesson.rake @@ -13,8 +13,70 @@ rescue TypeError puts "for production, we ignore type error" end +def generate + [*('a'..'z'),*('0'..'9')].shuffle[0,10].join.upcase +end + +def gc_10 + CSV.open("gift-card-10.csv", "wb") do |csv| + for i in 1..150 + csv << [generate()] + end + end +end + +def gc_20 + CSV.open("gift-card-20.csv", "wb") do |csv| + for i in 1..100 + csv << [generate()] + end + end +end + + +# round to. we make + +#One set of 200 codes that when redeemed translate into 5 (not 10) JamTracks each. + +#One set of 200 codes that when redeemed translate into 4 JamClass lessons each. + +def gc_5jt_2 + CSV.open("posa-cards-jt-5.csv", "wb") do |csv| + for i in 1..250 + csv << [generate()] + end + end +end + +def gc_4jc_2 + CSV.open("posa-cards-jc-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate()] + end + end +end + namespace :lessons do + task amazon_gift_cards: :environment do|task, args| + CSV.open("posa-cards-amazon-test-drive-paid-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-paid-4', true, true] + end + end + + CSV.open("posa-cards-amazon-test-drive-free-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-free-4',true, false] + end + end + + CSV.open("posa-cards-amazon-test-drive-free-2.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-free-2',true, false] + end + end + end task book_completed: :environment do |task, args| user = User.find_by_email(ENV['STUDENT']) @@ -68,7 +130,7 @@ namespace :lessons do end - booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", recurring, payment_style, 60) + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", recurring, payment_style, 30) if booking.errors.any? puts booking.errors.inspect raise "booking failed" diff --git a/web/posa_cards.rb b/web/posa_cards.rb new file mode 100644 index 000000000..c319f99d3 --- /dev/null +++ b/web/posa_cards.rb @@ -0,0 +1,66 @@ +def generate + [*('a'..'z'),*('0'..'9')].shuffle[0,10].join.upcase +end + +def gc_10 + CSV.open("gift-card-10.csv", "wb") do |csv| + for i in 1..150 + csv << [generate()] + end + end +end + +def gc_20 + CSV.open("gift-card-20.csv", "wb") do |csv| + for i in 1..100 + csv << [generate()] + end + end +end + + +# round to. we make + +#One set of 200 codes that when redeemed translate into 5 (not 10) JamTracks each. + +#One set of 200 codes that when redeemed translate into 4 JamClass lessons each. + +def gc_5jt_2 + CSV.open("posa-cards-jt-5.csv", "wb") do |csv| + for i in 1..250 + csv << [generate()] + end + end +end + +def gc_4jc_2 + CSV.open("posa-cards-jc-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate()] + end + end +end + + +def amazon_gift_cards + CSV.open("posa-cards-amazon-test-drive-paid-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-paid-4', true, true] + end + end + + CSV.open("posa-cards-amazon-test-drive-free-4.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-free-4',true, false] + end + end + + CSV.open("posa-cards-amazon-test-drive-free-2.csv", "wb") do |csv| + for i in 1..250 + csv << [generate(), 'amazon-test-drive-free-2',true, false] + end + end +end + + + diff --git a/web/spec/controllers/api_jamblasters_controller_spec.rb b/web/spec/controllers/api_jamblasters_controller_spec.rb index c8aeac195..44f66bc57 100644 --- a/web/spec/controllers/api_jamblasters_controller_spec.rb +++ b/web/spec/controllers/api_jamblasters_controller_spec.rb @@ -34,11 +34,12 @@ describe ApiJamblastersController, type: :controller do response.status.should == 200 end - it "disallows different user" do - user2 = FactoryGirl.create(:user) - get :is_allowed, {:format => 'json', jbid: jamblaster.client_id, user_id: user2.id} - response.status.should == 403 - end + + #it "disallows different user" do + # user2 = FactoryGirl.create(:user) + # get :is_allowed, {:format => 'json', jbid: jamblaster.client_id, user_id: user2.id} + # response.status.should == 403 + #end end describe "not already associated" do diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 2948f202d..464528e49 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -870,7 +870,41 @@ FactoryGirl.define do factory :posa_card, class: 'JamRuby::PosaCard' do sequence(:code) { |n| n.to_s } - card_type JamRuby::PosaCardType::JAM_TRACKS_5 + card_type JamRuby::PosaCard::JAM_TRACKS_5 + requires_purchase false + purchased true + + factory :posa_card_lesson_2 do + card_type JamRuby::PosaCard::JAM_CLASS_2 + credits 2 + lesson_package_type { JamRuby::LessonPackageType.test_drive_2 } + is_lesson true + end + + factory :posa_card_lesson_4 do + card_type JamRuby::PosaCard::JAM_CLASS_4 + credits 4 + lesson_package_type { JamRuby::LessonPackageType.test_drive_4 } + is_lesson true + end + + factory :amazon_2_free do + card_type JamRuby::PosaCard::JAM_CLASS_2 + credits 2 + lesson_package_type { JamRuby::LessonPackageType.amazon_test_drive_free_2 } + is_lesson true + preactivate true + end + + factory :amazon_4_paid do + card_type JamRuby::PosaCard::JAM_CLASS_4 + credits 2 + lesson_package_type { JamRuby::LessonPackageType.amazon_test_drive_paid_4 } + is_lesson true + requires_purchase true + purchased false + preactivate true + end end factory :posa_card_type, class: 'JamRuby::PosaCardType' do diff --git a/web/spec/features/activate_account_spec.rb b/web/spec/features/activate_account_spec.rb new file mode 100644 index 000000000..260f7bccb --- /dev/null +++ b/web/spec/features/activate_account_spec.rb @@ -0,0 +1,126 @@ +require 'spec_helper' + +# tests what happens when the websocket connection goes away +describe "Activate Account Card", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user1) { FactoryGirl.create(:user) } + + let(:amazon_2_free_card) { FactoryGirl.create(:amazon_2_free) } + + before(:all) do + User.delete_all + end + + describe "not logged in" do + describe "amazon_2_free_card" do + it "succeeds" do + visit '/account/activate/code' + + amazon_2_free_card.credits.should eql 2 + + find('h2', text: 'Activate Account') + fill_in "code", with: amazon_2_free_card.code + fill_in "email", with: "amzposa1@jamkazam.com" + fill_in "password", with: "jam123" + find('.redeem-container ins', visible: false).trigger(:click) + + find('button.redeem-giftcard', text: 'ACTIVATE ACCOUNT').trigger(:click) + + find('.jam-class.all-done span.amount-gifted', text: amazon_2_free_card.credits) + find('.done-action a.go-browse').trigger(:click) + + find('h2', text: 'search teachers') + + user = User.find_by_email("amzposa1@jamkazam.com") + amazon_2_free_card.reload + amazon_2_free_card.user.should eq(user) + amazon_2_free_card.requires_purchase.should be false + amazon_2_free_card.purchased.should be true + user.reload + user.jamclass_credits.should eq(amazon_2_free_card.credits) + end + + it "validates correctly" do + visit '/account/activate/code' + + find('h2', text: 'Activate Account') + + find('button.redeem-giftcard', text: 'ACTIVATE ACCOUNT').trigger(:click) + + find('.errors.active', text: "Email can't be blank") + + find('h2', text: 'Activate Account') + fill_in "code", with: amazon_2_free_card.code + fill_in "email", with: "amzpos2@jamkazam.com" + fill_in "password", with: "jam123" + find('.redeem-container ins', visible: false).trigger(:click) + + find('button.redeem-giftcard', text: 'ACTIVATE ACCOUNT').trigger(:click) + + find('.done-action a.go-browse').trigger(:click) + + find('h2', text: 'search teachers') + + user = User.find_by_email("amzpos2@jamkazam.com") + amazon_2_free_card.reload + amazon_2_free_card.user.should eq(user) + amazon_2_free_card.requires_purchase.should be false + amazon_2_free_card.purchased.should be true + user.reload + user.jamclass_credits.should eq(amazon_2_free_card.credits) + end + end + end + + + describe "logged in" do + it "succeeds" do + fast_signin(user1, '/account/activate/code') + + find('h2', text: 'Activate Account') + fill_in "code", with: amazon_2_free_card.code + + find('button.redeem-giftcard', text: 'ACTIVATE COUPON CODE').trigger(:click) + + find('.done-action a.go-browse').trigger(:click) + + find('h2', text: 'search teachers') + + user1.reload + amazon_2_free_card.reload + amazon_2_free_card.user.should eq(user) + amazon_2_free_card.requires_purchase.should be false + amazon_2_free_card.purchased.should be true + user.jamclass_credits.should eq(amazon_2_free_card.credits) + end + end + + describe "logged in" do + it "validates" do + fast_signin(user1, '/account/activate/code') + + find('h2', text: 'Activate Account') + + find('button.redeem-giftcard').trigger(:click) + + find('.errors.active', text: "Coupon Code does not exist") + + fill_in "code", with: amazon_2_free_card.code + + find('button.redeem-giftcard').trigger(:click) + + find('.done-action a.go-browse').trigger(:click) + + find('h2', text: 'search teachers') + + user1.reload + amazon_2_free_card.reload + amazon_2_free_card.user.should eq(user) + amazon_2_free_card.requires_purchase.should be false + amazon_2_free_card.purchased.should be true + user.jamclass_credits.should eq(amazon_2_free_card.credits) + end + end +end diff --git a/web/spec/features/book_test_drive_spec.rb b/web/spec/features/book_test_drive_spec.rb index df02c1afe..8b18ecd27 100644 --- a/web/spec/features/book_test_drive_spec.rb +++ b/web/spec/features/book_test_drive_spec.rb @@ -7,7 +7,7 @@ describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true let(:user) { FactoryGirl.create(:user, traditional_band: true,paid_sessions: true, paid_sessions_hourly_rate: 1, paid_sessions_daily_rate:1 ) } let(:teacher_user) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)} let(:teacher_user2) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)} - let(:card_lessons) {FactoryGirl.create(:posa_card, card_type: JamRuby::PosaCardType::JAM_CLASS_4)} + let(:card_lessons) {FactoryGirl.create(:posa_card_lesson_4)} let(:retailer) {FactoryGirl.create(:retailer)} before(:each) do diff --git a/web/spec/requests/users_controller_spec.rb b/web/spec/requests/users_controller_spec.rb index c52258efd..c5f34f5e4 100644 --- a/web/spec/requests/users_controller_spec.rb +++ b/web/spec/requests/users_controller_spec.rb @@ -12,6 +12,13 @@ describe UsersController, :type => :api do #login(authenticated_user.email, authenticated_user.password, 200, true) } + def login(user) + # login as fan + post '/api/auth_session.json', { :email => user.email, :password => user.password }.to_json, "CONTENT_TYPE" => 'application/json' + last_response.status.should == 200 + JSON.parse(last_response.body).should == { "success" => true } + end + it "unsubscribe" do user.subscribe_email.should eql true get '/unsubscribe/' + user.unsubscribe_token @@ -73,13 +80,13 @@ describe UsersController, :type => :api do describe "logged in" do it "should not set origin with no referrer info" do - controller.current_user = user + login(user) get :home response.cookies["origin"].should be_nil end it "should not set origin with referrer info" do - controller.current_user = user + login(user) get :home, utm_valid_cookie response.cookies["origin"].should be_nil end diff --git a/web/spec/spec_helper.rb b/web/spec/spec_helper.rb index ef5d9ac5f..6e0335f9b 100644 --- a/web/spec/spec_helper.rb +++ b/web/spec/spec_helper.rb @@ -92,7 +92,7 @@ Thread.new do :connect_time_stale_browser => 4, :connect_time_expire_browser => 6, :max_connections_per_user => 20, - :rabbitmq_host => 'localhost', + :rabbitmq_host => '127.0.0.1', :rabbitmq_port => 5672, :calling_thread => current, :cidr => ['0.0.0.0/0'], diff --git a/websocket-gateway/config/application.yml b/websocket-gateway/config/application.yml index 4a5d92a15..afb18f5af 100644 --- a/websocket-gateway/config/application.yml +++ b/websocket-gateway/config/application.yml @@ -25,13 +25,13 @@ development: test: port: 6759 verbose: true - rabbitmq_host: localhost + rabbitmq_host: 127.0.0.1 rabbitmq_port: 5672 <<: *defaults production: port: 6767 verbose: false - rabbitmq_host: localhost + rabbitmq_host: 127.0.0.1 rabbitmq_port: 5672 <<: *defaults