From 399d010310a984a5a5f7f0fcdfa5c023747c6dcb Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 5 Apr 2016 21:23:15 -0500 Subject: [PATCH] * lessons 90% done, but still mostly hidden --- admin/Gemfile | 3 + admin/app/admin/lesson_booking.rb | 47 + admin/app/admin/lesson_session.rb | 59 + admin/app/admin/monthly_stats.rb | 2 - admin/app/admin/students.rb | 220 +- admin/app/admin/teachers.rb | 16 +- .../app/controllers/application_controller.rb | 2 + admin/app/helpers/application_helper.rb | 1 + db/manifest | 3 +- db/up/lessons.sql | 261 ++ pb/src/client_container.proto | 31 + ruby/Gemfile | 7 +- ruby/lib/jam_ruby.rb | 21 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 1907 ++++++++---- .../invite_school_student.html.erb | 16 + .../invite_school_student.text.erb | 12 + .../invite_school_teacher.html.erb | 18 + .../invite_school_teacher.text.erb | 11 + .../monthly_recurring_done.html.erb | 28 + .../monthly_recurring_done.text.erb | 28 + .../monthly_recurring_no_bill.html.erb | 23 + .../monthly_recurring_no_bill.text.erb | 5 + .../student_lesson_accepted.html.erb | 24 + .../student_lesson_accepted.text.erb | 3 + .../student_lesson_booking_canceled.html.erb | 29 + .../student_lesson_booking_canceled.text.erb | 3 + .../student_lesson_booking_declined.html.erb | 20 + .../student_lesson_booking_declined.text.erb | 3 + .../student_lesson_canceled.html.erb | 23 + .../student_lesson_canceled.text.erb | 3 + .../student_lesson_counter.html.erb | 17 + .../student_lesson_counter.text.erb | 3 + .../student_lesson_monthly_charged.html.erb | 28 + .../student_lesson_monthly_charged.text.erb | 14 + .../student_lesson_monthly_done.html.erb | 28 + .../student_lesson_normal_done.html.erb | 29 + .../student_lesson_normal_done.text.erb | 16 + .../student_lesson_normal_no_bill.html.erb | 23 + .../student_lesson_normal_no_bill.text.erb | 5 + .../student_lesson_request.html.erb | 11 + .../student_lesson_request.text.erb | 3 + .../student_lesson_update_all.html.erb | 20 + .../student_lesson_update_all.text.erb | 3 + ...ent_scheduled_jamclass_invitation.html.erb | 11 + ...ent_scheduled_jamclass_invitation.text.erb | 7 + ...udent_test_drive_lesson_completed.html.erb | 30 + ...udent_test_drive_lesson_completed.text.erb | 14 + .../student_test_drive_lesson_done.html.erb | 47 + .../student_test_drive_lesson_done.text.erb | 8 + .../student_unable_charge.html.erb | 28 + .../student_unable_charge.text.erb | 18 + .../student_unable_charge_monthly.html.erb | 32 + .../student_unable_charge_monthly.text.erb | 23 + .../student_welcome_message.html.erb | 119 + .../student_welcome_message.text.erb | 43 + .../teacher_distribution_done.html.erb | 41 + .../teacher_distribution_done.text.erb | 31 + .../teacher_distribution_fail.html.erb | 15 + .../teacher_distribution_fail.text.erb | 12 + .../teacher_lesson_accepted.html.erb | 18 + .../teacher_lesson_accepted.text.erb | 3 + .../teacher_lesson_booking_canceled.html.erb | 29 + .../teacher_lesson_booking_canceled.text.erb | 3 + .../teacher_lesson_canceled.html.erb | 24 + .../teacher_lesson_canceled.text.erb | 3 + .../teacher_lesson_completed.html.erb | 17 + .../teacher_lesson_completed.text.erb | 3 + .../teacher_lesson_counter.html.erb | 17 + .../teacher_lesson_counter.text.erb | 3 + .../teacher_lesson_monthly_charged.html.erb | 23 + .../teacher_lesson_monthly_charged.text.erb | 9 + .../teacher_lesson_normal_done.html.erb | 23 + .../teacher_lesson_normal_done.text.erb | 11 + .../teacher_lesson_normal_no_bill.html.erb | 23 + .../teacher_lesson_normal_no_bill.text.erb | 5 + .../teacher_lesson_request.html.erb | 11 + .../teacher_lesson_request.text.erb | 3 + .../teacher_lesson_update_all.html.erb | 20 + .../teacher_lesson_update_all.text.erb | 3 + ...her_scheduled_jamclass_invitation.html.erb | 11 + ...her_scheduled_jamclass_invitation.text.erb | 7 + .../teacher_unable_charge_monthly.html.erb | 21 + .../teacher_unable_charge_monthly.text.erb | 14 + .../teacher_welcome_message.html.erb | 119 + .../teacher_welcome_message.text.erb | 43 + .../views/layouts/from_user_mailer.html.erb | 58 +- .../app/views/layouts/raw_mailer.html.erb | 72 + .../app/views/layouts/raw_mailer.text.erb | 7 + .../jam_ruby/constants/notification_types.rb | 3 + ruby/lib/jam_ruby/lib/stats.rb | 28 +- ruby/lib/jam_ruby/message_factory.rb | 53 +- .../jam_ruby/models/affiliate_distribution.rb | 19 + ruby/lib/jam_ruby/models/affiliate_partner.rb | 43 +- .../models/affiliate_payment_charge.rb | 51 + ruby/lib/jam_ruby/models/charge.rb | 134 + ruby/lib/jam_ruby/models/chat_message.rb | 28 +- ruby/lib/jam_ruby/models/invitation.rb | 16 +- ruby/lib/jam_ruby/models/jam_track.rb | 1 + ruby/lib/jam_ruby/models/lesson_booking.rb | 826 +++++ .../jam_ruby/models/lesson_booking_slot.rb | 239 ++ .../models/lesson_package_purchase.rb | 147 + .../jam_ruby/models/lesson_package_type.rb | 106 + .../jam_ruby/models/lesson_payment_charge.rb | 89 + ruby/lib/jam_ruby/models/lesson_session.rb | 642 ++++ .../models/lesson_session_analyser.rb | 307 ++ .../models/lesson_session_monthly_price.rb | 28 + ruby/lib/jam_ruby/models/music_session.rb | 20 +- .../models/music_session_user_history.rb | 8 +- ruby/lib/jam_ruby/models/notification.rb | 198 +- ruby/lib/jam_ruby/models/review.rb | 11 + ruby/lib/jam_ruby/models/sale.rb | 135 +- ruby/lib/jam_ruby/models/sale_line_item.rb | 36 +- ruby/lib/jam_ruby/models/school.rb | 107 + ruby/lib/jam_ruby/models/school_invitation.rb | 100 + ruby/lib/jam_ruby/models/shopping_cart.rb | 12 +- ruby/lib/jam_ruby/models/signup_hint.rb | 18 + ruby/lib/jam_ruby/models/teacher.rb | 42 +- .../jam_ruby/models/teacher_distribution.rb | 76 + ruby/lib/jam_ruby/models/teacher_intent.rb | 24 + ruby/lib/jam_ruby/models/teacher_payment.rb | 112 + .../jam_ruby/models/teacher_payment_charge.rb | 53 + ruby/lib/jam_ruby/models/user.rb | 274 +- .../lib/jam_ruby/models/user_authorization.rb | 4 + .../jam_ruby/resque/scheduled/hourly_job.rb | 18 + ruby/spec/factories.rb | 124 +- ruby/spec/jam_ruby/connection_manager_spec.rb | 1 - .../flows/monthly_recurring_lesson_spec.rb | 215 ++ .../spec/jam_ruby/flows/normal_lesson_spec.rb | 414 +++ .../jam_ruby/flows/recurring_lesson_spec.rb | 181 ++ .../jam_ruby/flows/testdrive_lesson_spec.rb | 219 ++ .../jam_ruby/models/affiliate_partner_spec.rb | 25 + .../models/lesson_booking_slot_spec.rb | 172 ++ .../jam_ruby/models/lesson_booking_spec.rb | 899 ++++++ .../models/lesson_package_purchase_spec.rb | 8 + .../models/lesson_session_analyser_spec.rb | 257 ++ .../lesson_session_monthly_price_spec.rb | 68 + .../jam_ruby/models/lesson_session_spec.rb | 49 + ruby/spec/jam_ruby/models/review_spec.rb | 2 +- ruby/spec/jam_ruby/models/sale_spec.rb | 300 +- .../jam_ruby/models/school_invitation_spec.rb | 35 + ruby/spec/jam_ruby/models/school_spec.rb | 30 + .../jam_ruby/models/teacher_payment_spec.rb | 293 ++ ruby/spec/jam_ruby/models/teacher_spec.rb | 26 +- ruby/spec/jam_ruby/models/user_spec.rb | 170 +- ruby/spec/mailers/render_emails_spec.rb | 113 +- ruby/spec/spec_helper.rb | 8 +- ruby/spec/support/lesson_session.rb | 79 + ruby/spec/support/utilities.rb | 91 +- web/Gemfile | 9 +- .../content/stripe-connect-blue-on-dark.png | Bin 0 -> 3378 bytes .../content/stripe-connect-light-on-dark.png | Bin 0 -> 2891 bytes .../images/landing/JK_FBAd_Bass_with_Keys.png | Bin 0 -> 402784 bytes .../assets/javascripts/AAB_message_factory.js | 3 + web/app/assets/javascripts/accounts.js | 7 + .../javascripts/accounts_profile_avatar.js | 2 +- web/app/assets/javascripts/application.js | 1 + web/app/assets/javascripts/dialog/banner.js | 33 +- web/app/assets/javascripts/jam_rest.js | 305 ++ web/app/assets/javascripts/jamkazam.js | 6 + web/app/assets/javascripts/layout.js | 21 +- .../assets/javascripts/notificationPanel.js | 43 + .../assets/javascripts/react-components.js | 4 + .../AccountSchoolScreen.js.jsx.coffee | 451 +++ .../AvatarEditLink.js.jsx.coffee | 55 + .../AvatarUploader.js.jsx.coffee | 168 ++ .../BookLessonFree.js.jsx.coffee | 524 ++++ .../ConfigureLiveTracksDialog.js.jsx.coffee | 22 +- .../ConfigureOutputsDialog.js.jsx.coffee | 4 +- .../EditableList.js.jsx.coffee | 4 +- .../InviteSchoolUserDialog.js.jsx.coffee | 124 + .../JamClassStudentScreen.js.jsx.coffee | 154 + .../JamTrackLandingScreen.js.jsx.coffee | 4 +- .../LessonBooking.js.jsx.coffee | 749 +++++ .../LessonBookingDecision.js.jsx.coffee | 233 ++ .../LessonPayment.js.jsx.coffee | 482 +++ .../LessonSession.js.jsx.coffee | 58 + .../react-components/Nav.js.jsx.coffee | 34 + .../SelectLocation.js.jsx.coffee | 8 +- .../SessionMediaTracks.js.jsx.coffee | 2 +- .../StripeConnect.js.jsx.coffee | 40 + ...eacherExperienceEditableList.js.jsx.coffee | 2 +- .../TeacherProfile.js.jsx.coffee | 3 +- .../TeacherSearchOptionsScreen.js.jsx.coffee | 11 +- .../TeacherSearchScreen.js.jsx.coffee | 96 +- .../TryTestDriveDialog.js.jsx.coffee | 99 + .../UploadAvatarDialog.js.jsx.coffee | 46 + .../actions/AvatarActions.js.coffee | 10 + .../actions/NavActions.js.coffee | 7 + .../actions/SchoolActions.js.coffee | 9 + .../actions/StripeActions.js.coffee | 6 + .../TeacherSearchResultsActions.js.coffee | 1 + .../actions/UserActions.js.coffee | 1 + ...ssAffiliateLandingBottomPage.js.jsx.coffee | 394 +++ ...JamClassAffiliateLandingPage.js.jsx.coffee | 139 + ...lassStudentLandingBottomPage.js.jsx.coffee | 55 +- .../SchoolStudentLandingPage.js.jsx.coffee | 147 + .../SchoolTeacherLandingPage.js.jsx.coffee | 147 + .../mixins/ICheckMixin.js.coffee | 56 + .../stores/AvatarStore.js.coffee | 298 ++ .../stores/NavStore.js.coffee | 46 + .../stores/SchoolStore.js.coffee | 77 + .../stores/StripeStore.js.coffee | 28 + .../TeacherSearchResultsStore.js.coffee | 25 +- .../stores/TeacherSearchStore.js.coffee | 3 +- .../stores/UserStore.js.coffee | 19 +- web/app/assets/javascripts/utils.js | 28 +- .../stylesheets/client/jamkazam.css.scss | 4 + .../client/jamtrack_landing.css.scss | 3 +- .../AccountSchoolScreen.css.scss | 261 ++ .../react-components/BookLessonFree.css.scss | 5 + .../JamClassStudentScreen.css.scss | 86 + .../react-components/LessonBooking.css.scss | 152 + .../LessonBookingScreen.css.scss | 239 ++ .../react-components/LessonPayment.css.scss | 122 + .../react-components/LessonSession.css.scss | 113 + .../react-components/TeacherProfile.css.scss | 6 + .../dialogs/inviteSchoolUser.css.scss | 33 + .../dialogs/tryTestDriveDialog.css.scss | 20 + .../dialogs/uploadAvatarDialog.css.scss | 70 + .../landings/school_landing.css.scss | 201 ++ web/app/controllers/api_chats_controller.rb | 9 +- web/app/controllers/api_controller.rb | 23 + .../controllers/api_jamblasters_controller.rb | 1 + .../api_lesson_bookings_controller.rb | 243 ++ .../api_lesson_sessions_controller.rb | 35 + web/app/controllers/api_reviews_controller.rb | 8 +- .../api_school_invitations_controller.rb | 62 + web/app/controllers/api_schools_controller.rb | 103 + .../api_signup_hints_controller.rb | 9 +- web/app/controllers/api_stripe_controller.rb | 21 + .../controllers/api_teachers_controller.rb | 6 + web/app/controllers/api_users_controller.rb | 9 +- web/app/controllers/clients_controller.rb | 2 +- web/app/controllers/landings_controller.rb | 89 +- web/app/controllers/popups_controller.rb | 2 +- web/app/controllers/sessions_controller.rb | 77 + web/app/controllers/users_controller.rb | 2 +- web/app/helpers/application_helper.rb | 5 + web/app/helpers/client_helper.rb | 1 + .../views/api_jam_tracks/show_for_client.rabl | 5 - web/app/views/api_lesson_bookings/accept.rabl | 3 + web/app/views/api_lesson_bookings/cancel.rabl | 3 + .../views/api_lesson_bookings/counter.rabl | 3 + .../api_lesson_bookings/create_free.rabl | 3 + .../api_lesson_bookings/create_paid.rabl | 3 + .../create_test_drive.rabl | 3 + web/app/views/api_lesson_bookings/intent.rabl | 20 + web/app/views/api_lesson_bookings/show.rabl | 32 + .../api_lesson_bookings/unprocessed.rabl | 3 + .../unprocessed_or_intent.rabl | 14 + web/app/views/api_lesson_sessions/index.rabl | 11 + web/app/views/api_lesson_sessions/show.rabl | 31 + .../views/api_school_invitations/create.rabl | 3 + .../views/api_school_invitations/index.rabl | 11 + .../views/api_school_invitations/resend.rabl | 3 + .../views/api_school_invitations/show.rabl | 7 + web/app/views/api_schools/delete_avatar.rabl | 3 + web/app/views/api_schools/remove_student.rabl | 3 + web/app/views/api_schools/remove_teacher.rabl | 3 + web/app/views/api_schools/show.rabl | 19 + web/app/views/api_schools/update.rabl | 3 + web/app/views/api_schools/update_avatar.rabl | 3 + web/app/views/api_stripe/store.rabl | 29 + web/app/views/api_teachers/intent.rabl | 3 + .../views/api_users/notification_index.rabl | 2 +- web/app/views/api_users/show.rabl | 18 +- .../views/api_users/test_drive_status.rabl | 8 + web/app/views/clients/_account.html.erb | 16 + .../views/clients/_account_school.html.slim | 9 + web/app/views/clients/index.html.erb | 6 + .../jamclass/_book_lesson_free.html.slim | 10 + .../jamclass/_jamclass_student.html.slim | 10 + .../jamclass/_lesson_booking.html.slim | 10 + .../jamclass/_lesson_payment.html.slim | 10 + .../jamclass/_lesson_session.html.slim | 10 + web/app/views/dialogs/_banner.html.slim | 3 +- web/app/views/dialogs/_dialogs.html.haml | 5 +- .../dialogs/_inviteSchoolUserDialog.html.slim | 2 + .../dialogs/_tryTestDriveDialog.html.slim | 2 + .../dialogs/_uploadAvatarDialog.html.slim | 2 + web/app/views/errors/stripe_error.rabl | 13 + .../landings/jam_class_affiliates.html.slim | 18 + .../landings/jam_class_students.html.slim | 5 +- .../school_student_register.html.slim | 5 + .../school_teacher_register.html.slim | 5 + web/app/views/layouts/client.html.erb | 1 + web/app/views/shared/_stripe.html.slim | 6 + web/config/application.rb | 20 +- web/config/environments/test.rb | 10 +- web/config/initializers/omniauth.rb | 1 + web/config/initializers/stripe.rb | 1 + web/config/initializers/zip_codes.rb | 1 + web/config/routes.rb | 36 +- web/config/sitemap.rb | 7 + web/lib/tasks/lesson.rake | 85 + web/lib/user_manager.rb | 6 +- .../api_school_invitations_controller_spec.rb | 58 + .../api_schools_controller_spec.rb | 33 + web/spec/factories.rb | 109 + web/spec/features/account_school_spec.rb | 46 + .../features/school_student_register_spec.rb | 108 + .../features/school_teacher_register_spec.rb | 109 + web/spec/features/student_landing_spec.rb | 61 + web/spec/features/teacher_landing_spec.rb | 61 + web/spec/managers/user_manager_spec.rb | 489 +-- .../assets/javascripts/jquery.inputmask.js | 2653 +++++++++++++++++ .../assets/javascripts/jquery.payment.js | 651 ++++ websocket-gateway/Gemfile | 4 + .../lib/jam_websockets/router.rb | 1 - 309 files changed, 22940 insertions(+), 1233 deletions(-) create mode 100644 admin/app/admin/lesson_booking.rb create mode 100644 admin/app/admin/lesson_session.rb create mode 100644 db/up/lessons.sql create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb create mode 100644 ruby/lib/jam_ruby/models/affiliate_distribution.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_payment_charge.rb create mode 100644 ruby/lib/jam_ruby/models/charge.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_booking.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_booking_slot.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_package_purchase.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_package_type.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_payment_charge.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_session.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_session_analyser.rb create mode 100644 ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb create mode 100644 ruby/lib/jam_ruby/models/school.rb create mode 100644 ruby/lib/jam_ruby/models/school_invitation.rb create mode 100644 ruby/lib/jam_ruby/models/teacher_distribution.rb create mode 100644 ruby/lib/jam_ruby/models/teacher_intent.rb create mode 100644 ruby/lib/jam_ruby/models/teacher_payment.rb create mode 100644 ruby/lib/jam_ruby/models/teacher_payment_charge.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb create mode 100644 ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb create mode 100644 ruby/spec/jam_ruby/flows/normal_lesson_spec.rb create mode 100644 ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb create mode 100644 ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_booking_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb create mode 100644 ruby/spec/jam_ruby/models/lesson_session_spec.rb create mode 100644 ruby/spec/jam_ruby/models/school_invitation_spec.rb create mode 100644 ruby/spec/jam_ruby/models/school_spec.rb create mode 100644 ruby/spec/jam_ruby/models/teacher_payment_spec.rb create mode 100644 ruby/spec/support/lesson_session.rb create mode 100644 web/app/assets/images/content/stripe-connect-blue-on-dark.png create mode 100644 web/app/assets/images/content/stripe-connect-light-on-dark.png create mode 100644 web/app/assets/images/landing/JK_FBAd_Bass_with_Keys.png create mode 100644 web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/InviteSchoolUserDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/Nav.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/NavActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/SchoolStudentLandingPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/AvatarStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/NavStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SchoolStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee create mode 100644 web/app/assets/stylesheets/client/react-components/AccountSchoolScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/BookLessonFree.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/LessonBooking.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/LessonPayment.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/LessonSession.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/inviteSchoolUser.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/tryTestDriveDialog.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/uploadAvatarDialog.css.scss create mode 100644 web/app/assets/stylesheets/landings/school_landing.css.scss create mode 100644 web/app/controllers/api_lesson_bookings_controller.rb create mode 100644 web/app/controllers/api_lesson_sessions_controller.rb create mode 100644 web/app/controllers/api_school_invitations_controller.rb create mode 100644 web/app/controllers/api_schools_controller.rb create mode 100644 web/app/controllers/api_stripe_controller.rb create mode 100644 web/app/views/api_lesson_bookings/accept.rabl create mode 100644 web/app/views/api_lesson_bookings/cancel.rabl create mode 100644 web/app/views/api_lesson_bookings/counter.rabl create mode 100644 web/app/views/api_lesson_bookings/create_free.rabl create mode 100644 web/app/views/api_lesson_bookings/create_paid.rabl create mode 100644 web/app/views/api_lesson_bookings/create_test_drive.rabl create mode 100644 web/app/views/api_lesson_bookings/intent.rabl create mode 100644 web/app/views/api_lesson_bookings/show.rabl create mode 100644 web/app/views/api_lesson_bookings/unprocessed.rabl create mode 100644 web/app/views/api_lesson_bookings/unprocessed_or_intent.rabl create mode 100644 web/app/views/api_lesson_sessions/index.rabl create mode 100644 web/app/views/api_lesson_sessions/show.rabl create mode 100644 web/app/views/api_school_invitations/create.rabl create mode 100644 web/app/views/api_school_invitations/index.rabl create mode 100644 web/app/views/api_school_invitations/resend.rabl create mode 100644 web/app/views/api_school_invitations/show.rabl create mode 100644 web/app/views/api_schools/delete_avatar.rabl create mode 100644 web/app/views/api_schools/remove_student.rabl create mode 100644 web/app/views/api_schools/remove_teacher.rabl create mode 100644 web/app/views/api_schools/show.rabl create mode 100644 web/app/views/api_schools/update.rabl create mode 100644 web/app/views/api_schools/update_avatar.rabl create mode 100644 web/app/views/api_stripe/store.rabl create mode 100644 web/app/views/api_teachers/intent.rabl create mode 100644 web/app/views/api_users/test_drive_status.rabl create mode 100644 web/app/views/clients/_account_school.html.slim create mode 100644 web/app/views/clients/jamclass/_book_lesson_free.html.slim create mode 100644 web/app/views/clients/jamclass/_jamclass_student.html.slim create mode 100644 web/app/views/clients/jamclass/_lesson_booking.html.slim create mode 100644 web/app/views/clients/jamclass/_lesson_payment.html.slim create mode 100644 web/app/views/clients/jamclass/_lesson_session.html.slim create mode 100644 web/app/views/dialogs/_inviteSchoolUserDialog.html.slim create mode 100644 web/app/views/dialogs/_tryTestDriveDialog.html.slim create mode 100644 web/app/views/dialogs/_uploadAvatarDialog.html.slim create mode 100644 web/app/views/errors/stripe_error.rabl create mode 100644 web/app/views/landings/jam_class_affiliates.html.slim create mode 100644 web/app/views/landings/school_student_register.html.slim create mode 100644 web/app/views/landings/school_teacher_register.html.slim create mode 100644 web/app/views/shared/_stripe.html.slim create mode 100644 web/config/initializers/stripe.rb create mode 100644 web/config/initializers/zip_codes.rb create mode 100644 web/lib/tasks/lesson.rake create mode 100644 web/spec/controllers/api_school_invitations_controller_spec.rb create mode 100644 web/spec/controllers/api_schools_controller_spec.rb create mode 100644 web/spec/features/account_school_spec.rb create mode 100644 web/spec/features/school_student_register_spec.rb create mode 100644 web/spec/features/school_teacher_register_spec.rb create mode 100644 web/spec/features/student_landing_spec.rb create mode 100644 web/spec/features/teacher_landing_spec.rb create mode 100644 web/vendor/assets/javascripts/jquery.inputmask.js create mode 100644 web/vendor/assets/javascripts/jquery.payment.js diff --git a/admin/Gemfile b/admin/Gemfile index 2c28dbf41..c6218e01d 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -77,6 +77,9 @@ gem 'influxdb', '0.1.8' gem 'influxdb-rails', '0.1.10' gem 'recurly' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' group :libv8 do gem 'libv8', "~> 4.5.95" diff --git a/admin/app/admin/lesson_booking.rb b/admin/app/admin/lesson_booking.rb new file mode 100644 index 000000000..b30d6ca8a --- /dev/null +++ b/admin/app/admin/lesson_booking.rb @@ -0,0 +1,47 @@ +ActiveAdmin.register JamRuby::LessonBooking, :as => 'LessonBookings' do + + menu :label => 'LessonBooking', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true ) { |scope| scope.unscoped.order('created_at desc') } + scope("Requested") { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended" ) { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled" ) { |scope| scope.unscoped.canceled.order('created_at desc') } + + index do + column "User Link" do |lesson_booking| + span do + link_to "Web URL", "#{Rails.application.config.external_root_url}/client#/jamclass/lesson-booking/#{lesson_booking.id}" + end + end + column "Type" do |lesson_booking| + lesson_booking.display_type + end + column "Status" do |lesson_booking| + lesson_booking.status + end + column "Teacher" do |lesson_booking| + teacher = lesson_booking.teacher + span do + link_to "#{teacher.name} (#{teacher.email})", "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.id}" + end + end + column "Student" do |lesson_booking| + student = lesson_booking.student + span do + link_to "#{student.name} (#{student.email})", "#{Rails.application.config.external_root_url}/client#/profile/#{student.id}" + end + end + end + + show do + + end + +end \ No newline at end of file diff --git a/admin/app/admin/lesson_session.rb b/admin/app/admin/lesson_session.rb new file mode 100644 index 000000000..c58eb4658 --- /dev/null +++ b/admin/app/admin/lesson_session.rb @@ -0,0 +1,59 @@ +ActiveAdmin.register JamRuby::LessonSession, :as => 'LessonSessions' do + + menu :label => 'LessonSession', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = false + config.per_page = 100 + config.paginate = true + config.filters = false + + scope("All", default: true) { |scope| scope.unscoped.order('created_at desc') } + scope("Requested" ) { |scope| scope.unscoped.where(status: LessonBooking::STATUS_REQUESTED).order('created_at desc') } + scope("Approved") { |scope| scope.unscoped.approved.order('created_at desc') } + scope("Suspended" ) { |scope| scope.unscoped.suspended.order('created_at desc') } + scope("Canceled" ) { |scope| scope.unscoped.canceled.order('created_at desc') } + scope("Missed" ) { |scope| scope.unscoped.missed.order('created_at desc') } + scope("Completed" ) { |scope| scope.unscoped.completed.order('created_at desc') } + + index do + column "User 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}" + end + end + column "Status" do |lesson_session| + lesson_session.status + end + column "Start Time" do |lesson_session| + span do + lesson_session.music_session.pretty_scheduled_start(true) + end + br + span do + lesson_session.music_session.scheduled_start + end + end + column "Duration" do |lesson_session| + lesson_session.duration + end + column "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}" + end + end + column "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}" + end + end + end + + show do + + end + +end \ No newline at end of file diff --git a/admin/app/admin/monthly_stats.rb b/admin/app/admin/monthly_stats.rb index b25990841..0a8e609bb 100644 --- a/admin/app/admin/monthly_stats.rb +++ b/admin/app/admin/monthly_stats.rb @@ -15,7 +15,6 @@ ActiveAdmin.register_page "Monthly Stats" do column "Sessions", :count end - h2 "Distinct Users Who Played with a JamTrack" table_for MusicSession.select([:month, :count]).find_by_sql("select date_trunc('month', jts.created_at)::date as month, count(distinct(user_id)) from jam_track_sessions jts group by month order by month desc;") do column "Month", Proc.new { |row| Date.parse(row.month).strftime('%B %Y') } @@ -36,5 +35,4 @@ ActiveAdmin.register_page "Monthly Stats" do end - end \ No newline at end of file diff --git a/admin/app/admin/students.rb b/admin/app/admin/students.rb index c9e91592f..a022aa727 100644 --- a/admin/app/admin/students.rb +++ b/admin/app/admin/students.rb @@ -1,4 +1,3 @@ -=begin ActiveAdmin.register JamRuby::User, :as => 'Students' do menu :label => 'Students', :parent => 'JamClass' @@ -9,66 +8,25 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do config.paginate = true def booked_anything(scope) - scope.joins(lesson_booking) + scope.joins(:student_lesson_bookings).where('lesson_bookings.active = true').uniq end - scope("Default", default: true) { |scope| booked_anything(scope) } + + scope("Default", default: true) { |scope| booked_anything(scope).order('ready_for_session_at IS NULL DESC') } index do - column "Name" do |teacher| - link_to teacher.user.name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" + column "Name" do |user| + link_to user.name, "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" end - column "Email" do |teacher| - teacher.user.email + column "Email" do |user| + user.email end - column "Location" do |teacher| - teacher.user.location(country = true) + column "Location" do |user| + user.location(country = true) end - column "Profile %" do |teacher| + + column "Session Ready" do |user| div do - span do - "#{teacher.pct_complete[:pct]}%" - end - br - span do - link_to "Detail", admin_teacher_path(teacher.id) - end - end - - end - column "Background Check" do |teacher| - div do - if teacher.background_check_at - span do - teacher.background_check_at.to_date - end - span do - br - end - span do - link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do - "mark as checked" - end - end - - else - span do - 'NOT DONE' - 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?"}) - end - end - - - end - end - - column "Session Ready" do |teacher| - div do - if teacher.ready_for_session + if user.ready_for_session_at span do 'YES' end @@ -80,166 +38,22 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do br end span do - link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) + link_to("mark as checked", mark_session_ready_admin_student_path(user.id), {confirm: "Mark as ready for session?"}) end end end end - column "Top Teacher" do |teacher| - div do - if teacher.top_rated - span do - 'YES' - end - span do - br - end - span do - link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) - end - end - end - - end - end - - show do - attributes_table do - row "Name" do |teacher| - link_to teacher.user.name, "#{Rails.application.config.external_root_url}/client#/profile/teacher/#{teacher.user.id}" - end - row "Email" do |teacher| - teacher.user.email - end - row "Location" do |teacher| - teacher.user.location(country = true) - end - row "Profile %" do |teacher| - div do - span do - "#{teacher.pct_complete[:pct]}%" - end - br - br - div do - h5 do "Completed Sections" end - teacher.pct_complete.each do |k, v| - if k != :pct && v - div do - k - end - end - end - br - br - h5 do "Uncompleted Sections" end - teacher.pct_complete.each do |k, v| - if k != :pct && !v - div do - k - end - end - end - end - end - - end - row "Background Check" do |teacher| - div do - if teacher.background_check_at - span do - teacher.background_check_at.to_date - end - span do - br - end - span do - link_to(mark_background_check_admin_teacher_path(teacher.id), {confirm: "Mark as background checked?"}) do - "mark as checked" - end - end - - else - span do - 'NOT DONE' - 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?"}) - end - end - - - end - end - - row "Session Ready" do |teacher| - div do - if teacher.ready_for_session - span do - 'YES' - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as checked", mark_session_ready_admin_teacher_path(teacher.id), {confirm: "Mark as ready for session?"}) - end - - end - end - end - row "Top Teacher" do |teacher| - div do - if teacher.top_rated - span do - 'YES' - end - span do - br - end - span do - link_to("mark not top", mark_not_top_admin_teacher_path(teacher.id), {confirm: "Mark as not top rated?"}) - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as top", mark_top_admin_teacher_path(teacher.id), {confirm: "Mark as top rated?"}) - end - end - end - + column "School" do |user| + if teacher.school + teacher.school.name end end end - member_action :mark_session_ready, :method => :get do resource.mark_session_ready redirect_to :back end -end -=end \ No newline at end of file +end \ No newline at end of file diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb index 9ffa7e3d9..942e501d5 100644 --- a/admin/app/admin/teachers.rb +++ b/admin/app/admin/teachers.rb @@ -45,6 +45,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=begin column "Background Check" do |teacher| div do if teacher.background_check_at @@ -73,6 +74,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end end +=end column "Session Ready" do |teacher| div do if teacher.ready_for_session_at @@ -122,7 +124,11 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do column "Signed Up" do |teacher| teacher.created_at.to_date end - + column "School" do |teacher| + if teacher.school + teacher.school.name + end + end end show do @@ -166,6 +172,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=begin row "Background Check" do |teacher| div do if teacher.background_check_at @@ -196,6 +203,7 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do end end +=end row "Session Ready" do |teacher| div do @@ -248,6 +256,12 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do teacher.created_at.to_date end + row "School" do |teacher| + if teacher.school + teacher.school.name + end + end + end end diff --git a/admin/app/controllers/application_controller.rb b/admin/app/controllers/application_controller.rb index 15e327c98..d3e00deb6 100644 --- a/admin/app/controllers/application_controller.rb +++ b/admin/app/controllers/application_controller.rb @@ -1,4 +1,6 @@ class ApplicationController < ActionController::Base + include ApplicationHelper + protect_from_forgery before_filter :prepare_gon diff --git a/admin/app/helpers/application_helper.rb b/admin/app/helpers/application_helper.rb index 6e9385e59..80731359d 100644 --- a/admin/app/helpers/application_helper.rb +++ b/admin/app/helpers/application_helper.rb @@ -1,4 +1,5 @@ module ApplicationHelper + end diff --git a/db/manifest b/db/manifest index 9c07236f3..4067937f3 100755 --- a/db/manifest +++ b/db/manifest @@ -340,4 +340,5 @@ jamblaster_pairing_active.sql email_blacklist.sql jamblaster_connection.sql teacher_progression.sql -teacher_complete.sql \ No newline at end of file +teacher_complete.sql +lessons.sql \ No newline at end of file diff --git a/db/up/lessons.sql b/db/up/lessons.sql new file mode 100644 index 000000000..bc6ff6daf --- /dev/null +++ b/db/up/lessons.sql @@ -0,0 +1,261 @@ + +CREATE TABLE lesson_package_types ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR NOT NULL, + description VARCHAR NOT NULL, + package_type VARCHAR(64) NOT NULL, + price NUMERIC(8,2), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_bookings ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + active BOOLEAN NOT NULL DEFAULT FALSE, + accepter_id VARCHAR(64) REFERENCES users(id), + canceler_id VARCHAR(64) REFERENCES users(id), + lesson_type VARCHAR(64) NOT NULL, + recurring BOOLEAN NOT NULL, + lesson_length INTEGER NOT NULL, + payment_style VARCHAR(64) NOT NULL, + description VARCHAR, + booked_price NUMERIC(8,2) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + card_presumed_ok BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + status VARCHAR, + cancel_message VARCHAR, + user_decremented BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE charges ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL DEFAULT 0, + type VARCHAR(64) NOT NULL, + sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_billing_notices_at TIMESTAMP, + last_billing_attempt_at TIMESTAMP, + billed BOOLEAN NOT NULL DEFAULT FALSE, + billed_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + billing_error_reason VARCHAR, + billing_error_detail VARCHAR, + billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE , + billing_attempts INTEGER NOT NULL DEFAULT 0, + stripe_charge_id VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE lesson_package_purchases ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id) NOT NULL, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id), + price NUMERIC(8,2), + recurring BOOLEAN NOT NULL DEFAULT FALSE, + year INTEGER, + month INTEGER, + charge_id VARCHAR(64) REFERENCES charges(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE lesson_sessions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_type VARCHAR(64) NOT NULL, + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + duration INTEGER NOT NULL, + booked_price NUMERIC(8,2) NOT NULL, + teacher_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_complete BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled BOOLEAN DEFAULT FALSE NOT NULL, + teacher_canceled BOOLEAN DEFAULT FALSE NOT NULL, + student_canceled_at TIMESTAMP, + teacher_canceled_at TIMESTAMP, + student_canceled_reason VARCHAR, + teacher_canceled_reason VARCHAR, + status VARCHAR, + analysed BOOLEAN NOT NULL DEFAULT FALSE, + analysis JSON, + analysed_at TIMESTAMP, + cancel_message VARCHAR, + canceler_id VARCHAR(64) REFERENCES users(id), + charge_id VARCHAR(64) REFERENCES charges(id), + success BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices BOOLEAN NOT NULL DEFAULT FALSE, + sent_notices_at TIMESTAMP, + post_processed BOOLEAN NOT NULL DEFAULT FALSE, + post_processed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200); +ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN; + +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00); +INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive', 'Test Drive', 'Four reduced-price lessons which you can use to find that ideal teacher.', 'test-drive', 49.99); + + +CREATE TABLE lesson_booking_slots ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + slot_type VARCHAR(64) NOT NULL, + preferred_day DATE, + day_of_week INTEGER, + hour INTEGER, + minute INTEGER, + timezone VARCHAR NOT NULL, + message VARCHAR, + accept_message VARCHAR, + update_all BOOLEAN NOT NULL DEFAULT FALSE, + proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_bookings ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); +ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id); + +ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id); +ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id); +ALTER TABLE users ADD COLUMN remaining_free_lessons INTEGER NOT NULL DEFAULT 1; +ALTER TABLE users ADD COLUMN stored_credit_card BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN remaining_test_drives INTEGER NOT NULL DEFAULT 0; +ALTER TABLE users ADD COLUMN stripe_token VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(200); +ALTER TABLE users ADD COLUMN stripe_zip_code VARCHAR(200); +ALTER TABLE sales ADD COLUMN stripe_charge_id VARCHAR(200); +ALTER TABLE teachers ADD COLUMN stripe_account_id VARCHAR(200); +ALTER TABLE sale_line_items ADD COLUMN lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id); + + +-- one is created every time the teacher is paid. N teacher_distributions point to this +CREATE TABLE teacher_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + charge_id VARCHAR(64) REFERENCES charges(id) NOT NULL, + amount_in_cents INTEGER NOT NULL, + fee_in_cents INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- one is created for every bit of money the teacher is due +CREATE TABLE teacher_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_payment_id VARCHAR(64) REFERENCES teacher_payments(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id), + lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id), + amount_in_cents INTEGER NOT NULL, + ready BOOLEAN NOT NULL DEFAULT FALSE, + distributed BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE affiliate_distributions ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) NOT NULL, + affiliate_referral_fee_in_cents INTEGER NOT NULL, + sale_line_item_id VARCHAR(64) REFERENCES sale_line_items(id) NOT NULL, + affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE, + affiliate_refunded_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE affiliate_partners ADD COLUMN lesson_rate NUMERIC (8,2) NOT NULL DEFAULT 0.20; + +-- move over all sale_line_item affiliate info +INSERT INTO affiliate_distributions ( + SELECT + sale_line_items.id, + sale_line_items.affiliate_referral_id, + sale_line_items.affiliate_referral_fee_in_cents, + sale_line_items.id, + sale_line_items.affiliate_refunded, + sale_line_items.affiliate_refunded_at, + sale_line_items.created_at, + sale_line_items.updated_at + FROM sale_line_items + WHERE sale_line_items.affiliate_referral_id IS NOT NULL +); + +CREATE TABLE teacher_intents ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + teacher_id VARCHAR(64) REFERENCES teachers(id) NOT NULL, + intent VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX teacher_intents_intent_idx ON teacher_intents(teacher_id, intent); + +CREATE TABLE schools ( + id INTEGER PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) NOT NULL, + name VARCHAR, + enabled BOOLEAN DEFAULT TRUE, + scheduling_communication VARCHAR NOT NULL DEFAULT 'teacher', + correspondence_email VARCHAR, + photo_url VARCHAR(2048), + original_fpfile VARCHAR(8000), + cropped_fpfile VARCHAR(8000), + cropped_s3_path VARCHAR(8000), + crop_selection VARCHAR(256), + large_photo_url VARCHAR(512), + cropped_large_s3_path VARCHAR(512), + cropped_large_fpfile VARCHAR(8000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE SEQUENCE school_key_sequence; +ALTER SEQUENCE school_key_sequence RESTART WITH 10000; +ALTER TABLE schools ALTER COLUMN id SET DEFAULT nextval('school_key_sequence'); + +ALTER TABLE users ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE users ADD COLUMN joined_school_at TIMESTAMP; +ALTER TABLE teachers ADD COLUMN school_id INTEGER REFERENCES schools(id); +ALTER TABLE teachers ADD COLUMN joined_school_at TIMESTAMP; + +CREATE TABLE school_invitations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + school_id INTEGER REFERENCES schools(id) NOT NULL, + invitation_code VARCHAR(256) NOT NULL UNIQUE, + note VARCHAR, + as_teacher BOOLEAN NOT NULL, + email VARCHAR NOT NULL, + first_name VARCHAR, + last_name VARCHAR, + accepted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE teachers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; +ALTER TABLE schools ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25; \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 1817f2241..57799fc7b 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -92,6 +92,9 @@ message ClientMessage { MIXDOWN_SIGN_COMPLETE = 270; MIXDOWN_SIGN_FAILED = 271; + LESSON_MESSAGE = 280; + SCHEDULED_JAMCLASS_INVITATION = 281; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -211,6 +214,9 @@ message ClientMessage { optional MixdownSignComplete mixdown_sign_complete = 270; optional MixdownSignFailed mixdown_sign_failed = 271; + // lesson notifications + optional LessonMessage lesson_message = 280; + optional ScheduledJamclassInvitation scheduled_jamclass_invitation = 281; // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -678,6 +684,31 @@ message MixdownSignFailed { required string mixdown_package_id = 1; // jam track mixdown package id } +message LessonMessage { + optional string music_session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string notification_id = 4; + optional string created_at = 5; + optional string sender_id = 6; + optional string receiver_id = 7; + optional bool student_directed = 8; + optional string purpose = 9; + optional string sender_name = 10; + optional string lesson_session_id = 11; +} + +message ScheduledJamclassInvitation { + optional string session_id = 1; + optional string photo_url = 2; + optional string msg = 3; + optional string session_name = 4; + optional string session_date = 5; + optional string notification_id = 6; + optional string created_at = 7; + optional string lesson_session_id = 8; +} + message SubscriptionMessage { optional string type = 1; // the type of the subscription diff --git a/ruby/Gemfile b/ruby/Gemfile index 0680c23cb..5c4c66424 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -52,6 +52,10 @@ gem 'sanitize' gem 'influxdb', '0.1.8' gem 'recurly' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'icalendar' +gem 'email_validator' group :test do gem 'simplecov', '~> 0.7.1' @@ -66,7 +70,8 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' - gem 'icalendar' + gem 'stripe-ruby-mock' + end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index d301a7cea..2fddb85f5 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -21,6 +21,9 @@ require 'rest-client' require 'zip' require 'csv' require 'tzinfo' +require 'stripe' +require 'zip-codes' +require 'email_validator' require "jam_ruby/constants/limits" require "jam_ruby/constants/notification_types" @@ -53,6 +56,7 @@ require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" require "jam_ruby/resque/scheduled/daily_job" +require "jam_ruby/resque/scheduled/hourly_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_reminder" @@ -268,10 +272,27 @@ require "jam_ruby/models/gift_card" require "jam_ruby/models/gift_card_purchase" require "jam_ruby/models/gift_card_type" require "jam_ruby/models/jam_track_session" +require "jam_ruby/models/lesson_package_type" +require "jam_ruby/models/lesson_package_purchase" +require "jam_ruby/models/lesson_session" +require "jam_ruby/models/lesson_booking" +require "jam_ruby/models/lesson_booking_slot" require "jam_ruby/models/jamblaster" require "jam_ruby/models/jamblaster_user" require "jam_ruby/models/jamblaster_pairing_request" require "jam_ruby/models/sale_receipt_ios" +require "jam_ruby/models/lesson_session_analyser" +require "jam_ruby/models/lesson_session_monthly_price" +require "jam_ruby/models/teacher_distribution" +require "jam_ruby/models/teacher_payment" +require "jam_ruby/models/charge" +require "jam_ruby/models/teacher_payment_charge" +require "jam_ruby/models/affiliate_payment_charge" +require "jam_ruby/models/lesson_payment_charge" +require "jam_ruby/models/affiliate_distribution" +require "jam_ruby/models/teacher_intent" +require "jam_ruby/models/school" +require "jam_ruby/models/school_invitation" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 40d5480f0..1612f140f 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -1,606 +1,1487 @@ - module JamRuby - # UserMailer must be configured to work - # Some common configs occur in jam_ruby/init.rb - # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), - # and in config/initializers/email.rb in rails to configure sendmail account settings - # If UserMailer were to be used in another project, it would need to be configured there, as well. +module JamRuby + # UserMailer must be configured to work + # Some common configs occur in jam_ruby/init.rb + # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode), + # and in config/initializers/email.rb in rails to configure sendmail account settings + # If UserMailer were to be used in another project, it would need to be configured there, as well. - # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer - class UserMailer < ActionMailer::Base - include SendGrid + # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer + class UserMailer < ActionMailer::Base + include SendGrid - layout "user_mailer" + layout "user_mailer" - DEFAULT_SENDER = "JamKazam " + DEFAULT_SENDER = "JamKazam " - default :from => DEFAULT_SENDER + default :from => DEFAULT_SENDER - sendgrid_category :use_subject_lines - #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) - sendgrid_unique_args :env => Environment.mode + sendgrid_category :use_subject_lines + #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth) + sendgrid_unique_args :env => Environment.mode - def confirm_email(user, signup_confirm_url) - @user = user - @signup_confirm_url = signup_confirm_url - sendgrid_category "Confirm Email" - sendgrid_unique_args :type => "confirm_email" + def confirm_email(user, signup_confirm_url) + @user = user + @signup_confirm_url = signup_confirm_url + sendgrid_category "Confirm Email" + sendgrid_unique_args :type => "confirm_email" - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) - mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| - format.text - format.html - end + mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format| + format.text + format.html + end + end + + def welcome_message(user) + @user = user + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def student_welcome_message(user) + @user = user + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def teacher_welcome_message(user) + @user = user + sendgrid_category "Welcome" + sendgrid_unique_args :type => "welcome_message" + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| + format.text + format.html + end + end + + def password_changed(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "password_changed" + mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| + format.text + format.html + end + end + + def password_reset(user, password_reset_url) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + @password_reset_url = password_reset_url + sendgrid_unique_args :type => "password_reset" + mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| + format.text + format.html + end + end + + def updating_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updating_email" + mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| + format.text + format.html + end + end + + def updated_email(user) + @user = user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + + sendgrid_unique_args :type => "updated_email" + mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| + format.text + format.html + end + end + + def new_musicians(user, new_musicians, host='www.jamkazam.com') + @user, @new_musicians, @host = user, new_musicians, host + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_unique_args :type => "new_musicians" + + mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| + format.text + format.html + end + end + + #################################### NOTIFICATION EMAILS #################################### + def friend_request(user, msg, friend_request_id) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend request on JamKazam" + unique_args = {:type => "friend_request"} + + @url = Nav.accept_friend_request_dialog(friend_request_id) + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def friend_request_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new friend on JamKazam" + unique_args = {:type => "friend_request_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_user_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have a new follower on JamKazam" + unique_args = {:type => "new_user_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def new_band_follower(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band has a new follower on JamKazam" + unique_args = {:type => "new_band_follower"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def session_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to a session on JamKazam" + unique_args = {:type => "session_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "Someone you know is in a session on JamKazam" + unique_args = {:type => "musician_session_join"} + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_invitation(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Invitation" + unique_args = {:type => "scheduled_session_invitation"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP" + unique_args = {:type => "scheduled_session_rsvp"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_approved(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Approved" + unique_args = {:type => "scheduled_session_rsvp_approved"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rsvp_cancelled_org(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Your Session RSVP Cancelled" + unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_cancelled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Cancelled" + unique_args = {:type => "scheduled_session_cancelled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_rescheduled(user, msg, session) + return if !user.subscribe_email + + email = user.email + subject = "Session Rescheduled" + unique_args = {:type => "scheduled_session_rescheduled"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_reminder_upcoming(user, session) + subject = "Your JamKazam session starts in 1 hour!" + unique_args = {:type => "scheduled_session_reminder_upcoming"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def scheduled_session_reminder_day(user, session) + subject = "JamKazam Session Reminder" + unique_args = {:type => "scheduled_session_reminder_day"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def send_scheduled_session_reminder(user, session, subject, unique_args) + return if !user.subscribe_email + + email = user.email + @user = user + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def scheduled_session_comment(target_user, sender, msg, comment, session) + return if !target_user.subscribe_email + + email = target_user.email + subject = "New Session Comment" + unique_args = {:type => "scheduled_session_comment"} + @body = msg + @session_name = session.name + @session_date = session.pretty_scheduled_start(true) + @comment = comment + @sender = sender + @suppress_user_has_account_footer = true + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [target_user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def scheduled_session_daily(receiver, sessions_and_latency) + sendgrid_category "Notification" + sendgrid_unique_args :type => "scheduled_session_daily" + + sendgrid_recipients([receiver.email]) + sendgrid_substitute('@USERID', [receiver.id]) + + @user = receiver + @sessions_and_latency = sessions_and_latency + + @title = 'New Scheduled Sessions Matched to You' + mail(:to => receiver.email, + :subject => EmailBatchScheduledSessions.subject) do |format| + format.text + format.html + end + end + + def band_session_join(user, msg, session_id) + return if !user.subscribe_email + + email = user.email + subject = "A band that you follow has joined a session" + unique_args = {:type => "band_session_join"} + + @body = msg + @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def musician_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A musician has saved a new recording on JamKazam" + unique_args = {:type => "musician_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_recording_saved(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "A band has saved a new recording on JamKazam" + unique_args = {:type => "band_recording_saved"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "You have been invited to join a band on JamKazam" + unique_args = {:type => "band_invitation"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + def band_invitation_accepted(user, msg) + return if !user.subscribe_email + + email = user.email + subject = "Your band invitation was accepted" + unique_args = {:type => "band_invitation_accepted"} + + @body = msg + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html + end + end + + + def text_message(user, sender_id, sender_name, sender_photo_url, message) + return if !user.subscribe_email + + email = user.email + subject = "Message from #{sender_name}" + unique_args = {:type => "text_message"} + + @note = message + @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) + @sender_id = sender_id + @sender_name = sender_name + @sender_photo_url = sender_photo_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_request(lesson_booking) + email = lesson_booking.user.email + subject = "You have sent a lesson request to #{lesson_booking.teacher.name}!" + unique_args = {:type => "student_lesson_request"} + + @sender = lesson_booking.teacher + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_booking.user.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_request(lesson_booking) + email = lesson_booking.teacher.email + subject = "You have received a lesson request through JamKazam!" + unique_args = {:type => "teacher_lesson_request"} + + @sender = lesson_booking.user + @lesson_booking = lesson_booking + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_booking.teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "Your have confirmed a lesson!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "Your lesson request is confirmed!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_accepted(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + @subject = "Your lesson time change is confirmed by #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + @subject = "You have confirmed a lesson!" end - def welcome_message(user) - @user = user - sendgrid_category "Welcome" - sendgrid_unique_args :type => "welcome_message" + @lesson_session = lesson_session + @message = message + email = lesson_session.teacher.email + unique_args = {:type => "teacher_lesson_accepted"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.student.name}!" + end + @lesson_session = lesson_session + @message = message + email = lesson_session.student.email + unique_args = {:type => "student_lesson_accepted"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_update_all(lesson_session, message, slot) + @slot = slot + if slot.is_teacher_created? + @target = lesson_session.student + @sender = lesson_session.teacher + subject = "All lesson times changed with #{lesson_session.student.name}!" + else + @target = lesson_session.teacher + @sender = lesson_session.student + subject = "All lesson times changed with #{lesson_session.student.name}!" end - def password_changed(user) - @user = user + @lesson_session = lesson_session + @message = message + email = lesson_session.teacher.email + unique_args = {:type => "teacher_lesson_update_all"} - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "password_changed" - mail(:to => user.email, :subject => "JamKazam Password Changed") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [lesson_session.teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def password_reset(user, password_reset_url) - @user = user + def teacher_scheduled_jamclass_invitation(user, msg, session) - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.student.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @password_reset_url = password_reset_url - sendgrid_unique_args :type => "password_reset" - mail(:to => user.email, :subject => "JamKazam Password Reset") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def updating_email(user) - @user = user + def student_scheduled_jamclass_invitation(user, msg, session) + return if !user.subscribe_email - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = user.email + @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.teacher.name}" + unique_args = {:type => "scheduled_jamclass_invitation"} + @student = session.lesson_session.student + @teacher = session.lesson_session.teacher + @body = msg + @session_name = session.name + @session_description = session.description + @session_date = session.pretty_scheduled_start(true) + @session_url = session.lesson_session.web_url + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updating_email" - mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [user.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html end + end - def updated_email(user) - @user = user + # teacher proposed counter time; so send msg to the student + def student_lesson_counter(lesson_session, slot) - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) + email = lesson_session.student.email + subject = "Instructor has proposed a different time for your lesson" + unique_args = {:type => "student_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_unique_args :type => "updated_email" - mail(:to => user.email, :subject => "JamKazam Email Changed") do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def new_musicians(user, new_musicians, host='www.jamkazam.com') - @user, @new_musicians, @host = user, new_musicians, host - sendgrid_recipients([user.email]) - sendgrid_substitute('@USERID', [user.id]) - sendgrid_unique_args :type => "new_musicians" + # student proposed counter time; so send msg to the teacher + def teacher_lesson_counter(lesson_session, slot) - mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| - format.text - format.html - end + email = lesson_session.teacher.email + subject = "Student has proposed a different time for their lesson" + unique_args = {:type => "teacher_lesson_counter"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - #################################### NOTIFICATION EMAILS #################################### - def friend_request(user, msg, friend_request_id) - return if !user.subscribe_email + def teacher_lesson_completed(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.teacher.email + subject = "You successfully completed a lesson with #{@student.name}" + unique_args = {:type => "teacher_lesson_completed"} - email = user.email - subject = "You have a new friend request on JamKazam" - unique_args = {:type => "friend_request"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @url = Nav.accept_friend_request_dialog(friend_request_id) - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def friend_request_accepted(user, msg) - return if !user.subscribe_email + # successfully completed, and has some remaining test drives + def student_test_drive_lesson_completed(lesson_session) - email = user.email - subject = "You have a new friend on JamKazam" - unique_args = {:type => "friend_request_accepted"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def new_user_follower(user, msg) - return if !user.subscribe_email + # successfully completed, but no more test drives left + def student_test_drive_lesson_done(lesson_session) - email = user.email - subject = "You have a new follower on JamKazam" - unique_args = {:type => "new_user_follower"} + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "You have used all 4 TestDrive lesson credits" + unique_args = {:type => "student_test_drive_success"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "raw_mailer" } end + end - def new_band_follower(user, msg) - return if !user.subscribe_email + def student_lesson_normal_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session - email = user.email - subject = "Your band has a new follower on JamKazam" - unique_args = {:type => "new_band_follower"} + email = @student.email + subject = "Your lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_no_bill"} - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def session_invitation(user, msg) - return if !user.subscribe_email + def teacher_lesson_normal_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.teacher.email + subject = "Your student #{@student.name} will not be charged for their lesson" + unique_args = {:type => "teacher_lesson_normal_no_bill"} - email = user.email - subject = "You have been invited to a session on JamKazam" - unique_args = {:type => "session_invitation"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def musician_session_join(user, msg, session_id) - return if !user.subscribe_email + def student_lesson_normal_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session - email = user.email - subject = "Someone you know is in a session on JamKazam" - unique_args = {:type => "musician_session_join"} - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "Your JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "student_lesson_normal_done"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_invitation(user, msg, session) - return if !user.subscribe_email + def teacher_lesson_normal_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.teacher.email + subject = "Your JamClass lesson today with #{@student.first_name}" + unique_args = {:type => "teacher_lesson_normal_done"} - email = user.email - subject = "Session Invitation" - unique_args = {:type => "scheduled_session_invitation"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp(user, msg, session) - return if !user.subscribe_email + def student_unable_charge(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + @card_declined = lesson_session.is_card_declined? + @card_expired = lesson_session.is_card_expired? + @bill_date = lesson_session.last_billed_at_date - email = user.email - subject = "Session RSVP" - unique_args = {:type => "scheduled_session_rsvp"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + email = @student.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "student_unable_charge"} - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp_approved(user, msg, session) - return if !user.subscribe_email + def teacher_unable_charge(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + email = lesson_session.teacher.email + subject = "The credit card charge for your lesson today with #{@teacher.name} failed" + unique_args = {:type => "teacher_lesson_normal_done"} - email = user.email - subject = "Session RSVP Approved" - unique_args = {:type => "scheduled_session_rsvp_approved"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } end + end - def scheduled_session_rsvp_cancelled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_rsvp_cancelled_org(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Your Session RSVP Cancelled" - unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_cancelled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session Cancelled" - unique_args = {:type => "scheduled_session_cancelled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_rescheduled(user, msg, session) - return if !user.subscribe_email - - email = user.email - subject = "Session Rescheduled" - unique_args = {:type => "scheduled_session_rescheduled"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_reminder_upcoming(user, session) - subject = "Your JamKazam session starts in 1 hour!" - unique_args = {:type => "scheduled_session_reminder_upcoming"} - send_scheduled_session_reminder(user, session, subject, unique_args) - end - - def scheduled_session_reminder_day(user, session) - subject = "JamKazam Session Reminder" - unique_args = {:type => "scheduled_session_reminder_day"} - send_scheduled_session_reminder(user, session, subject, unique_args) - end - - def send_scheduled_session_reminder(user, session, subject, unique_args) - return if !user.subscribe_email - - email = user.email - @user = user - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def scheduled_session_comment(target_user, sender, msg, comment, session) - return if !target_user.subscribe_email - - email = target_user.email - subject = "New Session Comment" - unique_args = {:type => "scheduled_session_comment"} - @body = msg - @session_name = session.name - @session_date = session.pretty_scheduled_start(true) - @comment = comment - @sender = sender - @suppress_user_has_account_footer = true - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [target_user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end - end - - def scheduled_session_daily(receiver, sessions_and_latency) - sendgrid_category "Notification" - sendgrid_unique_args :type => "scheduled_session_daily" - - sendgrid_recipients([receiver.email]) - sendgrid_substitute('@USERID', [receiver.id]) - - @user = receiver - @sessions_and_latency = sessions_and_latency - - @title = 'New Scheduled Sessions Matched to You' - mail(:to => receiver.email, - :subject => EmailBatchScheduledSessions.subject) do |format| - format.text - format.html - end - end - - def band_session_join(user, msg, session_id) - return if !user.subscribe_email - - email = user.email - subject = "A band that you follow has joined a session" - unique_args = {:type => "band_session_join"} - - @body = msg - @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def musician_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A musician has saved a new recording on JamKazam" - unique_args = {:type => "musician_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_recording_saved(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "A band has saved a new recording on JamKazam" - unique_args = {:type => "band_recording_saved"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "You have been invited to join a band on JamKazam" - unique_args = {:type => "band_invitation"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end - end - - def band_invitation_accepted(user, msg) - return if !user.subscribe_email - - email = user.email - subject = "Your band invitation was accepted" - unique_args = {:type => "band_invitation_accepted"} - - @body = msg - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] - - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) - - mail(:to => email, :subject => subject) do |format| - format.text - format.html - end + def student_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@teacher.name} have been suspended." + else + @subject = "The credit card charge for your #{@month_name} lessons with #{@teacher.name} failed." end - def text_message(user, sender_id, sender_name, sender_photo_url, message) - return if !user.subscribe_email + unique_args = {:type => "student_unable_charge_monthly"} - email = user.email - subject = "Message from #{sender_name}" - unique_args = {:type => "text_message"} + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] - @note = message - @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id}) - @sender_id = sender_id - @sender_name = sender_name - @sender_photo_url = sender_photo_url - sendgrid_category "Notification" - sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) - sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end - mail(:to => email, :subject => subject) do |format| - format.text - format.html { render :layout => "from_user_mailer" } - end + def teacher_unable_charge_monthly(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = @teacher.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." end - # def send_notification(email, subject, msg, unique_args) - # @body = msg - # sendgrid_category "Notification" - # sendgrid_unique_args :type => unique_args[:type] - # mail(:bcc => email, :subject => subject) do |format| - # format.text - # format.html - # end - # end - ############################################################################################# + unique_args = {:type => "teacher_unable_charge_monthly"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + email = @student.email + @subject = "Your JamClass lessons with #{@teacher.first_name} for #{@month_name}" + + unique_args = {:type => "student_lesson_monthly_charged"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_monthly_charged(lesson_package_purchase) + lesson_booking = lesson_package_purchase.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_package_purchase = lesson_package_purchase + @card_declined = lesson_package_purchase.is_card_declined? + @card_expired = lesson_package_purchase.is_card_expired? + @bill_date = lesson_package_purchase.last_billed_at_date + @lesson_booking = lesson_booking + @month_name = lesson_package_purchase.month_name + + email = @teacher.email + if lesson_booking.is_suspended? + @subject = "Your weekly lessons with #{@student.name} has been suspended." + else + @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}." + end + + + unique_args = {:type => "student_lesson_monthly_charged"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_distribution_done(teacher_payment) + @teacher_payment = teacher_payment + @teacher = teacher_payment.teacher + email = @teacher.email + + @subject = "You have received payment for your participation in JamClass" + unique_args = {:type => "teacher_distribution_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def teacher_distribution_fail(teacher_payment) + @teacher_payment = teacher_payment + @teacher = teacher_payment.teacher + email = @teacher.email + + @card_declined = teacher_payment.is_card_declined? + @card_expired = teacher_payment.is_card_expired? + @bill_date = teacher_payment.last_billed_at_date + + + @subject = "We were unable to pay you today" + unique_args = {:type => "teacher_distribution_fail"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def monthly_recurring_done(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your JamClass lesson today with #{@teacher.first_name}" + unique_args = {:type => "student_lesson_normal_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def monthly_recurring_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your lesson with #{@teacher.name} will not be billed" + unique_args = {:type => "student_lesson_normal_done"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_booking_declined(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "We're sorry your lesson request has been declined" + unique_args = {:type => "student_lesson_booking_declined"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "student_lesson_booking_canceled"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_booking_canceled(lesson_booking, message) + @lesson_booking = lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @teacher.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_booking_canceled"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def student_lesson_canceled(lesson_session, message) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + email = @student.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "student_lesson_canceled"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def teacher_lesson_canceled(lesson_session, message) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @teacher.email + @subject = "Your lesson has been canceled" + unique_args = {:type => "teacher_lesson_canceled"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def invite_school_teacher(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school + + email = school_invitation.email + @subject = "#{@school.owner.name} has sent you an invitation to join #{@school.name} on JamKazam" + unique_args = {:type => "invite_school_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end + end + + def invite_school_student(school_invitation) + @school_invitation = school_invitation + @school = school_invitation.school + + email = school_invitation.email + @subject = "#{@school.name} has sent you an invitation to join JamKazam for lessons" + unique_args = {:type => "invite_school_student"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + + @suppress_user_has_account_footer = true + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html + end end end +end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb new file mode 100644 index 000000000..164315809 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +

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

+
+

+ SIGN + UP NOW +

+
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb new file mode 100644 index 000000000..1b1ea765b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can +register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the link +below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of +<%= @school.name %>, welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb new file mode 100644 index 000000000..6e59f21d8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +
+

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

+
+

+ SIGN + UP NOW +

+ +
+
+Best Regards,
+Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb new file mode 100644 index 000000000..c31475ea8 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +Hello <%= @school_invitation.first_name %> - +<%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music +lessons in an amazing new way that really works. To accept this invitation, please click the link below, +and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam! + +<%= @school_invitation.generate_signup_url %> + +Best Regards, +Team JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb new file mode 100644 index 000000000..96bf19889 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb new file mode 100644 index 000000000..96bf19889 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb @@ -0,0 +1,28 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb new file mode 100644 index 000000000..3dc58d8ec --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb new file mode 100644 index 000000000..f21f164b5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb new file mode 100644 index 000000000..d60afb1dc --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + This teacher has accepted your lesson request! + <% else %> + You have confirmed a lesson request. + <% end %> + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..bc819bb5e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @teacher.name %> have been canceled. + + <% else %> + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

<%= @lesson_booking.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb new file mode 100644 index 000000000..410621edb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ + This teacher has declined your lesson request. + + <% if @message.present? %> +

<%= @teacher.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view the teacher's response.

+

+ VIEW RESPONSE +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_declined.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb new file mode 100644 index 000000000..484bccb78 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

<%= @lesson_session.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW RESPONSE +

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

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

+

+ VIEW + LESSON DETAILS +

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

+ Hello <%= @student.name %>, +

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..7328cf9a7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. + +<% if !@student.has_rated_teacher(@teacher) %> +If you haven't already done so, please rate your teachernow to help other students in the community find the best instructors. <%= @teacher.ratings_url %> +<% end %> + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb new file mode 100644 index 000000000..d795f64b4 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb new file mode 100644 index 000000000..1a3c7e42a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been + billed $<%= @lesson_session.amount_charged %> for today's lesson. +

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. + <% end %> + + If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb new file mode 100644 index 000000000..efdccf098 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb @@ -0,0 +1,16 @@ +Hello <%= @student.name %>, + +We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson. + +<% if !@student.has_rated_teacher(@teacher) %> + If you haven't already done so, please rate your teacher now to help other students in the community find the best + instructors. You can rate your teacher here: <%= @teacher.ratings_url %> +<% end %> + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb new file mode 100644 index 000000000..f7836f6ce --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You will not be billed for today's session with <%= @teacher.name %> +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..0c6d60a65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You will not be billed for today's session with <%= @teacher.name %> + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb new file mode 100644 index 000000000..4636052b0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson requested of #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

You have requested a <%= @lesson_booking.display_type %> lesson.

Click the button below to see your lesson request. You will receive another email when the teacher accepts or rejects the request.

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb new file mode 100644 index 000000000..9ec7f863e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_request.text.erb @@ -0,0 +1,3 @@ +You have requested a lesson from <%= @sender.name %>. + +To see this lesson request, click here: <%= @lesson_booking.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb new file mode 100644 index 000000000..d0259e567 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.teacher.name %> have been rescheduled. + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to get more information and to update your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb new file mode 100644 index 000000000..6011f7e65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.teacher.name%> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb new file mode 100644 index 000000000..67590256e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb new file mode 100644 index 000000000..c0e1bcb4d --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -0,0 +1,30 @@ +<% provide(:title, "You have used #{@student.remaining_test_drives} of 4 TestDrive lesson credits") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

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

+ +

+ <% if !@student.has_rated_teacher(@teacher) %> + Also, please rate your teacher now for today’s lesson + to help other students in the community find the best instructors. + <% end %> + And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to + improve JamClass, + please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb new file mode 100644 index 000000000..37ea61020 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -0,0 +1,14 @@ +You have used <%= @student.remaining_test_drives %> of 4 TestDrive lesson credits. + +<% if @student.has_rated_teacher(@teacher) %> + Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. +<% end %> + +If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the +link to your favorite to book single or weekly recurring lessons with the best instructor for you! +<% @student.recent_test_drive_teachers.each do |teacher| %> +<%= teacher.name %>: <%= teacher.teacher_profile_url %> +<% end %> + +And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, +please email us at support@jamkazam.com. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb new file mode 100644 index 000000000..b5e14e612 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb @@ -0,0 +1,47 @@ +<% provide(:title, "You have used all 4 TestDrive lesson credits") %> + +

+ Hello <%= @student.name %>, +

+ +

+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have now used all 4 TestDrive credits. + + <% if !@student.has_rated_teacher(@teacher) %> + Please rate your teacher now for today’s lesson to + help other students in the community find the best instructors. + <% end %> +

+

+ If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the + link to your favorite to book single or weekly recurring lessons with the best instructor for you! +

+<% @student.recent_test_drive_teachers.each do |teacher| %> + + + + + + +
+

+ +

+ <%= teacher.name %> +

+
+ +<% end %> + +

+ And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how + to improve JamClass, + please email us at support@jamkazam.com.

+
+ +

+ Best Regards,
Team JamKazam +

+ + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb new file mode 100644 index 000000000..2d4d02829 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb @@ -0,0 +1,8 @@ +You have used all of your 4 TestDrive lesson credits. + +<% if @student.has_rated_teacher(@teacher) %> + Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. +<% end %> + +And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, +please email us at support@jamkazam.com. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb new file mode 100644 index 000000000..2da383332 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb @@ -0,0 +1,28 @@ +<% provide(:title, "The credit card charge for your lesson today with #{@teacher.first_name} failed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ + <% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> + +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb new file mode 100644 index 000000000..b4cfb4628 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb @@ -0,0 +1,18 @@ +The credit card charge for your lesson today with <%= @teacher.first_name %> failed. +Hello <%= @student.name %>, + +<% if @card_declined %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_session.update_payment_url %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb new file mode 100644 index 000000000..f4d5f5402 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb @@ -0,0 +1,32 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +
+
+ <% end %> + + <% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! + <% end %> +

+

+ UPDATE PAYMENT INFO +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb new file mode 100644 index 000000000..2567d6891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb @@ -0,0 +1,23 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed. +<% end %> + +<% if @card_declined %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% elsif @card_expired %> + When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% else %> + For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you! +<% end %> + + +Update Payment info here: <%= @lesson_booking.update_payment_url %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb new file mode 100644 index 000000000..177f38c92 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -0,0 +1,119 @@ +<% provide(:title, 'Welcome to JamKazam!') %> + + +<% if !@user.anonymous? %> +

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

+<% end %> + + +

We're delighted you have decided to join the JamKazam community of musicians, and we hope + + you will enjoy using JamKazam and JamTracks to play more music. Following are some + + resources and some things you can do to get the most out of JamKazam. +

+ + +

Playing With JamTracks
+ + JamTracks are the best way to play along with your favorite songs. Far better and different than + + traditional backing tracks, our JamTracks are complete multi-track professional recordings, with + + fully isolated tracks for each part of the music. And our free app and Internet service are packed + + with features that give you unmatched creative freedom to learn, practice, record, play with + + others, and share your performances. Here are some great JamTracks resources: +

+
    +
  • JamTracks Overview Video - See all the great things you can do with JamTracks.
  • +
  • JamTracks User Guide - A set of articles that explain how to use all the JamTracks + + features.
  • +
  • Get a JamTrack Free - A web page you can visit to search our catalog of JamTracks. + + When you find a song you like, click the Get It Free button, and your first one is free! If + + you already redeemed a free JamTrack or purchased JamTracks, you can also access + + them on this page from your web browser.
  • +
  • JamKazam Application - A web page where you can download our free Mac or Windows + + app. The app lets you do a lot more with JamTracks than you can do in a browser.
  • +
+ +

Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over + + the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio + + quality. You can set up online sessions that are public or private, for you alone or for others to + + join. You can find and join others’ sessions, use backing tracks and loops in sessions, make + + audio and video recordings of session performances, and more. Click here for a set of tutorial + + videos that show how to use these features. +

+ +

Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how + + unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and + + student playing together at all. JamKazam is a terrific service for teaching and taking online + + music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and + + your students in getting set up and ready to go. +

+ +

Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who + + you are as a musician, as well as your musical interests. For example, what instruments do you + + play? What musical genres do you like best? Are you interested in getting into a virtual/online + + or a real-world band? And so on. Filling out your profile will help you connect with others with + + common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then + + click on the Profile tile, and click the Edit Profile button. +

+ +

Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do + + this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the + + user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in + + this menu, and you can then use the options to invite your friends using their email addresses, + + or via Facebook, or using your Google contacts. +

+ +

Get Help
+ + If you run into trouble and need help, please reach out to us. We will be glad to do everything + + we can to answer your questions and get you up and running. You can visit our Support Portal + + to find knowledge base articles and post questions that have not already been answered. You + + can email us at support@jamkazam.com. And if you just want to chat, share tips and war + + stories, and hang out with fellow JamKazamers, you can visit our Community Forum. +

+ +

+
+
+ Again, welcome to JamKazam, and we hope you have a great time here! +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb new file mode 100644 index 000000000..52dc8ab65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb @@ -0,0 +1,43 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. + + +Getting Started +--------------- + +There are basically three kinds of setups you can use to play on JamKazam. + +* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way. + +* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here. + +* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product. + + +JamKazam Features +----------------- + +JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam: + +* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk + +* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU + +* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA + +* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc + +* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY + + +Getting Help +------------ + +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/. + +Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb new file mode 100644 index 000000000..af2681325 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb @@ -0,0 +1,41 @@ +<% provide(:title, @subject) %> + +

You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:

+
+ +<% @teacher_payment.teacher_distributions.each do |distribution| %> + + <% if distribution.is_test_drive? %> +

You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>

+

+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% elsif distribution.is_normal? %> +

You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>

+

+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% elsif distribution.is_monthly? %> +

You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>

+

+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> + If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. + <% end %> + If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +

+ <% else %> + Unknown payment type. + <% end %> +
+
+ +<% end %> + +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb new file mode 100644 index 000000000..2e18fa163 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb @@ -0,0 +1,31 @@ +<% provide(:title, @subject) %> + +You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details: + +<% @teacher_payment.teacher_distributions.each do |distribution| %> + +<% if distribution.is_test_drive? %> +You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>. +<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% elsif distribution.is_normal? %> +You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>. +<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% elsif distribution.is_monthly? %> +You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>. +<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %> +If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %> +<% end%> +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. +<% else %> +Unknown payment type. +<% end %> +<% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb new file mode 100644 index 000000000..bdd291b49 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb @@ -0,0 +1,15 @@ +<% provide(:title, @subject) %> + +

+ <% if @card_declined %> + When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! + <% elsif @card_expired %> + When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% else %> + For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! + <% end %> +

+
+ +Best Regards,
+JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb new file mode 100644 index 000000000..8cc72bce5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> + + <% if @card_declined %> +When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you! + <% elsif @card_expired %> +When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you! + <% else %> +For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you! + <% end %> + +Best Regards, +JamKazam \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb new file mode 100644 index 000000000..7e76b8c0c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @slot.is_teacher_approved? %> + You have confirmed a lesson request. + <% else %> + This student has accepted your lesson request! + <% end %> +

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb new file mode 100644 index 000000000..6823a39f7 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb @@ -0,0 +1,3 @@ +You have confirmed a lesson request for <%= @sender.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb new file mode 100644 index 000000000..c0095a170 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb @@ -0,0 +1,29 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ <% if @lesson_booking.recurring %> + + All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @student.name %> have been canceled. + + <% else %> + Your lesson with <%= @student.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + <% end %> + + <% if @message.present? %> +

<%= @lesson_booking.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb new file mode 100644 index 000000000..4a60fe695 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.canceler.resolved_photo_url) %> + +<% content_for :note do %> +

+ + Your lesson with <%= @teacher.name %> has been canceled.

+ + Session Name: <%= @session_name %>
+ Session Description: <%= @session_description %>
+ <%= @session_date %> + + <% if @message.present? %> +

<%= @lesson_session.canceler.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view more info about the canceled session.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb new file mode 100644 index 000000000..c7506b0ad --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb @@ -0,0 +1,3 @@ +<%= @subject %> + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb new file mode 100644 index 000000000..e895471a3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, "You successfully completed a lesson with #{@student.name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ <%= @student.name %> will first be billed and you should receive your payment in the next 48 hours. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

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

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

+

+ VIEW + LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb new file mode 100644 index 000000000..050917a31 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb @@ -0,0 +1,3 @@ +<%= @student.name %> has proposed a different time for their lesson request. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb new file mode 100644 index 000000000..5b8bc3891 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. +

+ +

+ If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb new file mode 100644 index 000000000..0347dc3be --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb @@ -0,0 +1,9 @@ +Hello <%= @teacher.name %>, + +Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours. + +If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb new file mode 100644 index 000000000..8ec5b345c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your JamClass lesson today with #{@student.first_name}") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ Your student <%= @student.name %> will be billed for today's lesson, and you should receive payment within 48 hours. +

+ +

+ If you had technical problems during your lesson, or have questions, or would like to make suggestions + on how to improve JamClass, please email us at support@jamkazam.com. +

+
+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb new file mode 100644 index 000000000..08db0f67e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb @@ -0,0 +1,11 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will be billed for today's lesson, and you will receive payment within 24 hours. + +If you had technical problems during your lesson, or have questions, or would like to make suggestions +on how to improve JamClass, please email us at support@jamkazam.com. + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb new file mode 100644 index 000000000..7b24c238e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your student #{@student.name} will not be charged for their lesson") %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

Your student <%= @student.name %> will not be billed for today's session. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb new file mode 100644 index 000000000..9b9f3b91c --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @teacher.name %>, + +Your student <%= @student.name %> will not be billed for today's session. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb new file mode 100644 index 000000000..a82e118cb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, "Lesson Request from #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

This student has requested to schedule a <%= @lesson_booking.display_type %> lesson.

Click the button below to get more information and to respond to this lesson request. You must respond to this lesson request promptly, or it will be cancelled, thank you!

+

+ VIEW LESSON REQUEST +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb new file mode 100644 index 000000000..c28cf0142 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.text.erb @@ -0,0 +1,3 @@ +<%= @sender.name %> has requested a lesson. + +To see this lesson request, click here: <%= @lesson_booking.home_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb new file mode 100644 index 000000000..ff9c4a3b6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %> + +<% content_for :note do %> +

+ All lessons with <%= @lesson_session.student.name %> have been rescheduled. + + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to get more information and to update your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

+

+ VIEW LESSON DETAILS +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb new file mode 100644 index 000000000..5fc1c92a5 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb @@ -0,0 +1,3 @@ +All your lessons with <%= @lesson_session.student.name %> have been rescheduled. + +To see this lesson, click here: <%= @lesson_session.web_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb new file mode 100644 index 000000000..67590256e --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb @@ -0,0 +1,11 @@ +<% provide(:title, @subject) %> + +

<%= @body %>

+ +

+ Session Name: <%= @session_name %>
+ <%= @session_description %>
+ <%= @session_date %> +

+ +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb new file mode 100644 index 000000000..63679c0d2 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb @@ -0,0 +1,7 @@ +<%= @body %> + +<%= @session_name %> +<%= @session_description %> +<%= @session_date %> + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb new file mode 100644 index 000000000..fbee04816 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb @@ -0,0 +1,21 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @teacher.name %>, +

+ +

+ <% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. + <% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. + <% end %> +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb new file mode 100644 index 000000000..8807cbbda --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb @@ -0,0 +1,14 @@ +Hello <%= @student.name %>, + +<%= @subject %> + +<% if @lesson_booking.is_suspended? %> + Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info. +<% else %> + We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though. +<% end %> + +Best Regards, +Team JamKazam + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb new file mode 100644 index 000000000..177f38c92 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb @@ -0,0 +1,119 @@ +<% provide(:title, 'Welcome to JamKazam!') %> + + +<% if !@user.anonymous? %> +

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

+<% end %> + + +

We're delighted you have decided to join the JamKazam community of musicians, and we hope + + you will enjoy using JamKazam and JamTracks to play more music. Following are some + + resources and some things you can do to get the most out of JamKazam. +

+ + +

Playing With JamTracks
+ + JamTracks are the best way to play along with your favorite songs. Far better and different than + + traditional backing tracks, our JamTracks are complete multi-track professional recordings, with + + fully isolated tracks for each part of the music. And our free app and Internet service are packed + + with features that give you unmatched creative freedom to learn, practice, record, play with + + others, and share your performances. Here are some great JamTracks resources: +

+
    +
  • JamTracks Overview Video - See all the great things you can do with JamTracks.
  • +
  • JamTracks User Guide - A set of articles that explain how to use all the JamTracks + + features.
  • +
  • Get a JamTrack Free - A web page you can visit to search our catalog of JamTracks. + + When you find a song you like, click the Get It Free button, and your first one is free! If + + you already redeemed a free JamTrack or purchased JamTracks, you can also access + + them on this page from your web browser.
  • +
  • JamKazam Application - A web page where you can download our free Mac or Windows + + app. The app lets you do a lot more with JamTracks than you can do in a browser.
  • +
+ +

Play Live With Others from Different Locations on JamKazam
+ JamKazam’s free app lets musicians play together live and in sync from different locations over + + the Internet. Kind of like Skype on super audio steroids, with ultra low latency and terrific audio + + quality. You can set up online sessions that are public or private, for you alone or for others to + + join. You can find and join others’ sessions, use backing tracks and loops in sessions, make + + audio and video recordings of session performances, and more. Click here for a set of tutorial + + videos that show how to use these features. +

+ +

Teach or Take Online Music Lessons
+ If you teach music lessons and have tried to give lessons using Skype, you’ll know how + + unsatisfactory that experience is. Audio quality is poor, and latency prohibits teacher and + + student playing together at all. JamKazam is a terrific service for teaching and taking online + + music lessons. If you want to use JamKazam for lessons, we’ll be happy to support both you and + + your students in getting set up and ready to go. +

+ +

Complete Your Profile
+ Every member of our community has a profile. It’s a great way to share a little bit about who + + you are as a musician, as well as your musical interests. For example, what instruments do you + + play? What musical genres do you like best? Are you interested in getting into a virtual/online + + or a real-world band? And so on. Filling out your profile will help you connect with others with + + common interests. To do this, go to www.jamkazam.com or launch the JamKazam app. Then + + click on the Profile tile, and click the Edit Profile button. +

+ +

Invite Your Friends
+ Have friends who are musicians? Invite them to join you to play together on JamKazam. To do + + this, go to www.jamkazam.com or launch the JamKazam app. Then move your mouse over the + + user icon in the top right corner of the screen. A menu will be displayed. Click Invite Friends in + + this menu, and you can then use the options to invite your friends using their email addresses, + + or via Facebook, or using your Google contacts. +

+ +

Get Help
+ + If you run into trouble and need help, please reach out to us. We will be glad to do everything + + we can to answer your questions and get you up and running. You can visit our Support Portal + + to find knowledge base articles and post questions that have not already been answered. You + + can email us at support@jamkazam.com. And if you just want to chat, share tips and war + + stories, and hang out with fellow JamKazamers, you can visit our Community Forum. +

+ +

+
+
+ Again, welcome to JamKazam, and we hope you have a great time here! +

+ +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb new file mode 100644 index 000000000..52dc8ab65 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb @@ -0,0 +1,43 @@ +<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %> + +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. + + +Getting Started +--------------- + +There are basically three kinds of setups you can use to play on JamKazam. + +* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way. + +* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here. + +* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product. + + +JamKazam Features +----------------- + +JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam: + +* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk + +* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU + +* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA + +* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc + +* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY + + +Getting Help +------------ + +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/. + +Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! + +Best Regards, +Team JamKazam + diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb index 5b7ce4aec..c80b0490c 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb @@ -5,13 +5,14 @@ JamKazam @@ -25,15 +26,22 @@ - + +

<%= yield(:title) %>

+

+ <%= yield(:title) %>

+

<%= yield %>


- +

+

+ +

<%= yield(:note) %>

-
+
@@ -43,24 +51,30 @@ <% unless @suppress_user_has_account_footer == true %> - - - - - + + +
+
+ + +
- -

-

This email was sent to you because you have an account at JamKazam. -

+ +

+

+ This email was sent to you because + you have an account at JamKazam. +

- - + + <% end %> -
Copyright © <%= Time.now.year %> JamKazam, Inc. All rights reserved. + + Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb new file mode 100644 index 000000000..67ce3ee7f --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.html.erb @@ -0,0 +1,72 @@ + + + + + JamKazam + + + + + + + + + + +
JamKazam
+ + + + + + + + + + <% unless @suppress_user_has_account_footer == true %> + + + + <% end %> +

+ <%= yield(:title) %>

+ + + <%= yield %> + + +
+ + + + +
+ + +

+

+ This email was sent to you because + you have an account at JamKazam. +

+ +
+ + + + +
+ Copyright © <%= Time.now.year %> JamKazam, + Inc. All rights reserved. +
+ + + diff --git a/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb new file mode 100644 index 000000000..dbc71434a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/layouts/raw_mailer.text.erb @@ -0,0 +1,7 @@ +<%= yield %> + +<% unless @user.nil? || @suppress_user_has_account_footer == true %> +This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. To unsubscribe: https://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>. +<% end %> + +Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved. diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index 60dfe9f1b..465d59e37 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -54,4 +54,7 @@ module NotificationTypes MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + # jamclass + LESSON_MESSAGE = "LESSON_MESSAGE" + SCHEDULED_JAMCLASS_INVITATION = "SCHEDULED_JAMCLASS_INVITATION" end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/stats.rb b/ruby/lib/jam_ruby/lib/stats.rb index 697965526..17506a27e 100644 --- a/ruby/lib/jam_ruby/lib/stats.rb +++ b/ruby/lib/jam_ruby/lib/stats.rb @@ -82,8 +82,32 @@ module JamRuby return if self.ignore # doing any writes in a test environment cause annoying puts to occur if @client && data && data.length > 0 - data['host'] = @host - data['time'] = Time.now.to_i + if data.has_key?('values') || data.has_key?(:values) + @client.write_point(name, data) + data['timestamp'] = Time.now.to_i + + tags = data['tags'] + key = 'tags' if tags + tags ||= data[:tags] + key = :tags if key.nil? + tags ||= {} + key = :tags if key.nil? + + tags['host'] = @host + data[key] = tags + else + tags = {} + values = {} + for k,v in data + if v.is_a?(String) + tags[k] = v + else + values[k] = v + end + end + data = {tags: tags, values: values} + end + @client.write_point(name, data) end end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 0cb05e60e..30763c028 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -524,21 +524,41 @@ module JamRuby ) end - def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) - scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + def scheduled_jamclass_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at, lesson_session_id) + scheduled_jamclas_invitation = Jampb::ScheduledJamclassInvitation.new( :session_id => session_id, :photo_url => photo_url, :msg => msg, :session_name => session_name, :session_date => session_date, :notification_id => notification_id, - :created_at => created_at + :created_at => created_at, + lesson_session_id: lesson_session_id ) Jampb::ClientMessage.new( - :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :type => ClientMessage::Type::SCHEDULED_JAMCLASS_INVITATION, :route_to => USER_TARGET_PREFIX + receiver_id, - :scheduled_session_invitation => scheduled_session_invitation + :scheduled_jamclass_invitation => scheduled_jamclas_invitation + ) + end + + + def scheduled_session_invitation(receiver_id, session_id, photo_url, msg, session_name, session_date, notification_id, created_at) + scheduled_session_invitation = Jampb::ScheduledSessionInvitation.new( + :session_id => session_id, + :photo_url => photo_url, + :msg => msg, + :session_name => session_name, + :session_date => session_date, + :notification_id => notification_id, + :created_at => created_at + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::SCHEDULED_SESSION_INVITATION, + :route_to => USER_TARGET_PREFIX + receiver_id, + :scheduled_session_invitation => scheduled_session_invitation ) end @@ -949,6 +969,29 @@ module JamRuby ) end + # creates the general purpose text message + def lesson_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, notification_id, music_session_id, created_at, student_directed, purpose, lesson_session_id) + lesson_message = Jampb::LessonMessage.new( + :photo_url => sender_photo_url, + :sender_name => sender_name, + :sender_id => sender_id, + :receiver_id => receiver_id, + :msg => msg, + :notification_id => notification_id, + :music_session_id => music_session_id, + :created_at => created_at, + :student_directed => student_directed, + :purpose => purpose, + :lesson_session_id => lesson_session_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LESSON_MESSAGE, + :route_to => USER_TARGET_PREFIX + receiver_id, + :lesson_message => lesson_message + ) + end + # creates the chat message def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel) chat_message = Jampb::ChatMessage.new( diff --git a/ruby/lib/jam_ruby/models/affiliate_distribution.rb b/ruby/lib/jam_ruby/models/affiliate_distribution.rb new file mode 100644 index 000000000..68988748e --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_distribution.rb @@ -0,0 +1,19 @@ +module JamRuby + class AffiliateDistribution < ActiveRecord::Base + + + belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem' + belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + validates :affiliate_referral, presence:true + validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false} + + def self.create(affiliate_referral, fee_in_cents, sale_line_item) + distribution = AffiliateDistribution.new + distribution.affiliate_referral = affiliate_referral + distribution.affiliate_referral_fee_in_cents = fee_in_cents + distribution.sale_line_item = sale_line_item + distribution + end + end +end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index e88aed9c7..aaf05fc35 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -9,6 +9,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :affiliate_distributions, :class_name => "JamRuby::AffiliateDistribution", foreign_key: :affiliate_referral_id attr_accessible :partner_name, :partner_code, :partner_user_id, :entity_type, :rate, as: :admin ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } @@ -118,17 +119,19 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base sale_time - user.created_at < 2.years end - def should_attribute_sale?(shopping_cart) + def should_attribute_sale?(shopping_cart, user_to_check, instance) - if created_within_affiliate_window(shopping_cart.user, Time.now) - product_info = shopping_cart.product_info + if created_within_affiliate_window(user_to_check, Time.now) + product_info = shopping_cart.product_info(instance) # subtract the total quantity from the freebie quantity, to see how much we should attribute to them real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i - {fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round} + + applicable_rate = shopping_cart.is_lesson? ? lesson_rate : rate + + {fee_in_cents: (product_info[:price] * 100 * real_quantity * applicable_rate.to_f).round} else false end - end def cumulative_earnings_in_dollars @@ -233,23 +236,23 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base def self.sale_items_subquery(start_date, end_date, table_name) %{ - FROM sale_line_items + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.created_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.created_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id } end def self.sale_items_refunded_subquery(start_date, end_date, table_name) %{ - FROM sale_line_items + FROM affiliate_distributions inner join sale_line_items ON affiliate_distributions.sale_line_item_id = sale_line_items.id WHERE - (DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}')) + (DATE(affiliate_distributions.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(affiliate_distributions.affiliate_refunded_at) <= DATE('#{end_date}')) AND - sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + affiliate_distributions.affiliate_referral_id = #{table_name}.affiliate_partner_id AND - sale_line_items.affiliate_refunded = TRUE + affiliate_distributions.affiliate_refunded = TRUE } end # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id @@ -269,22 +272,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base last_updated = NOW(), jamtracks_sold = COALESCE( - (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) + COALESCE( - (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0), due_amount_in_cents = COALESCE( - (SELECT SUM(affiliate_referral_fee_in_cents) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} ), 0) @@ -322,22 +325,22 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base last_updated = NOW(), jamtracks_sold = COALESCE( - (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) + COALESCE( - (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND affiliate_distributions.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0), due_amount_in_cents = COALESCE( - (SELECT SUM(affiliate_referral_fee_in_cents) + (SELECT SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) + COALESCE( - (SELECT -SUM(affiliate_referral_fee_in_cents) + (SELECT -SUM(affiliate_distributions.affiliate_referral_fee_in_cents) #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} ), 0) diff --git a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb new file mode 100644 index 000000000..7955fc3a1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb @@ -0,0 +1,51 @@ +module JamRuby + class AffiliatePaymentCharge < Charge + + has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.teacher + end + + def charged_user + teacher + end + + def do_charge + + # source will let you supply a token. But... how to get a token in this case? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + UserMailer.teacher_distribution_done(teacher_payment) + end + + def do_send_unable_charge + UserMailer.teacher_distribution_fail(teacher_payment) + end + + def construct_description + teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb new file mode 100644 index 000000000..5af2b3b5a --- /dev/null +++ b/ruby/lib/jam_ruby/models/charge.rb @@ -0,0 +1,134 @@ +module JamRuby + class Charge < ActiveRecord::Base + + validates :sent_billing_notices, inclusion: {in: [true, false]} + + def max_retries + raise "not implemented" + end + def do_charge(force) + raise "not implemented" + end + def do_send_notices + raise "not implemented" + end + def do_send_unable_charge + raise "not implemented" + end + def charge_retry_hours + 24 + end + def charged_user + raise "not implemented" + end + + def charge(force = false) + + stripe_charge = nil + + if !self.billed + + # check if we can bill at the moment + if !force && last_billing_attempt_at && (charge_retry_hours.hours.ago < last_billing_attempt_at) + return false + end + + if !force && !billing_should_retry + return false + end + + + # bill the user right now. if it fails, move on; will be tried again + self.billing_attempts = self.billing_attempts + 1 + self.billing_should_retry = self.billing_attempts < max_retries + self.last_billing_attempt_at = Time.now + self.save(validate: false) + + begin + + stripe_charge = do_charge(force) + self.stripe_charge_id = stripe_charge.id + self.billed = true + self.billed_at = Time.now + self.save(validate: false) + rescue Stripe::StripeError => e + + stripe_handler(e) + + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (stripe)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + do_send_unable_charge + + return false + rescue Exception => e + subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (unhandled)" + body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}" + AdminMailer.alerts({subject: subject, body: body}) + unhandled_handler(e) + return false + end + + end + + if !self.sent_billing_notices + # If the charge is successful, then we post the charge to the student’s payment history, + # and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email + + do_send_notices + + self.sent_billing_notices = true + self.sent_billing_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(validate: false) + end + + return stripe_charge + end + + def unhandled_handler(e, reason = 'unhandled_exception') + self.billing_error_reason = reason + if e.cause + self.billing_error_detail = e.cause.to_s + "\n" + e.cause.backtrace.join("\n\t") if e.cause.backtrace + self.billing_error_detail << "\n\n" + self.billing_error_detail << e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + else + self.billing_error_detail = e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace + end + self.save(validate: false) + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + def stripe_handler(e) + + msg = e.to_s + + if msg.include?('declined') + self.billing_error_reason = 'card_declined' + self.billing_error_detail = msg + elsif msg.include?('expired') + self.billing_error_reason = 'card_expired' + self.billing_error_detail = msg + elsif msg.include?('processing') + self.billing_error_reason = 'processing_error' + self.billing_error_detail = msg + else + self.billing_error_reason = 'stripe' + self.billing_error_detail = msg + end + + self.save(validate: false) + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 8e650bad7..3c6b056db 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -3,18 +3,42 @@ module JamRuby include HtmlSanitize html_sanitize strict: [:message] + CHANNEL_LESSON = 'lesson' self.table_name = 'chat_messages' self.primary_key = 'id' default_scope order('created_at DESC') + attr_accessor :ignore_message_checks + attr_accessible :user_id, :message, :music_session_id belongs_to :user belongs_to :music_session + belongs_to :target_user, class_name: "JamRuby::User" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" validates :user, presence: true - validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true + validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true, unless: :ignore_message_checks + + def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_booking = nil) + chat_msg = ChatMessage.new + chat_msg.user_id = user.id + chat_msg.music_session_id = music_session.id if music_session + chat_msg.message = message + chat_msg.channel = channel + chat_msg.target_user = target_user + chat_msg.lesson_booking = lesson_booking + + if lesson_booking + chat_msg.ignore_message_checks = true + end + + if chat_msg.save + ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel + end + chat_msg + end class << self @@ -36,7 +60,7 @@ module JamRuby query = ChatMessage.where('music_session_id = ?', music_session_id) end - query = query.offset(start).limit(limit).order('created_at DESC') + query = query.offset(start).limit(limit).order('created_at DESC').includes([:user]) if query.length == 0 [query, nil] diff --git a/ruby/lib/jam_ruby/models/invitation.rb b/ruby/lib/jam_ruby/models/invitation.rb index b31a816f6..45c24505f 100644 --- a/ruby/lib/jam_ruby/models/invitation.rb +++ b/ruby/lib/jam_ruby/models/invitation.rb @@ -1,6 +1,7 @@ module JamRuby class Invitation < ActiveRecord::Base + INVITATION_NOT_TEACHER_VALIDATION_ERROR = "Lessons can only sent invitations to teachers" FRIENDSHIP_REQUIRED_VALIDATION_ERROR = "You can only invite friends" MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION = "You must be a member of the music session to send invitations on behalf of it" JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION = "You can only associate a join request with an invitation if that join request comes from the invited user and if it's for the same music session" @@ -15,7 +16,7 @@ module JamRuby validates :receiver, :presence => true validates :music_session, :presence => true - validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join + validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join_or_teacher private @@ -25,15 +26,22 @@ module JamRuby end end - def require_are_friends_or_requested_to_join + def require_are_friends_or_requested_to_join_or_teacher if !join_request.nil? && (join_request.user != receiver || join_request.music_session != music_session) errors.add(:join_request, JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION ) elsif join_request.nil? # we only check for friendship requirement if this was not in response to a join_request - unless receiver.friends.exists? sender - errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + if !receiver.friends.exists?(sender) && (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + if !receiver.friends.exists?(sender) + errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR) + elsif (music_session.is_lesson? && receiver != music_session.lesson_session.teacher) + errors.add(:receiver, INVITATION_NOT_TEACHER_VALIDATION_ERROR) + end + + end end + end end end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 1ad7fb09d..72febb08c 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -338,6 +338,7 @@ module JamRuby query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') + query = query.includes([{ jam_track_tracks: :instrument }, { genres_jam_tracks: :genre }]) else query = query.group("jam_tracks.id") if options[:sort_by] == 'jamtrack' diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb new file mode 100644 index 000000000..384009aaa --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -0,0 +1,826 @@ +# represenst the type of lesson package +module JamRuby + class LessonBooking < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:description, :cancel_message] + + include ActiveModel::Dirty + + @@log = Logging.logger[LessonBooking] + + attr_accessor :accepting, :countering, :countered_slot, :countered_lesson + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED] + + LESSON_TYPE_FREE = 'single-free' + LESSON_TYPE_TEST_DRIVE = 'test-drive' + LESSON_TYPE_PAID = 'paid' + + LESSON_TYPES = [LESSON_TYPE_FREE, LESSON_TYPE_TEST_DRIVE, LESSON_TYPE_PAID] + + PAYMENT_STYLE_ELSEWHERE = 'elsewhere' + PAYMENT_STYLE_SINGLE = 'single' + PAYMENT_STYLE_WEEKLY = 'weekly' + PAYMENT_STYLE_MONTHLY = 'monthly' + + + PAYMENT_STYLES = [PAYMENT_STYLE_ELSEWHERE, PAYMENT_STYLE_SINGLE, PAYMENT_STYLE_WEEKLY, PAYMENT_STYLE_MONTHLY] + + belongs_to :user, class_name: "JamRuby::User" + belongs_to :teacher, class_name: "JamRuby::User" + belongs_to :accepter, class_name: "JamRuby::User" + belongs_to :canceler, class_name: "JamRuby::User" + belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking + + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + has_many :lesson_sessions, class_name: "JamRuby::LessonSession" + has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase" + + validates :user, presence: true + validates :teacher, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :recurring, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :card_presumed_ok, inclusion: {in: [true, false]} + validates :active, inclusion: {in: [true, false]} + validates :lesson_length, inclusion: {in: [30, 45, 60, 90, 120]} + validates :payment_style, inclusion: {in: PAYMENT_STYLES} + validates :booked_price, presence: true + validates :description, no_profanity: true, length: {minimum: 10, maximum: 20000} + + validate :validate_user, on: :create + validate :validate_recurring + validate :validate_lesson_booking_slots + validate :validate_lesson_length + validate :validate_payment_style + validate :validate_accepted, :if => :accepting + + + before_save :before_save + before_validation :before_validation + after_create :after_create + around_save :around_update + + scope :test_drive, -> { where(lesson_type: LESSON_TYPE_TEST_DRIVE) } + scope :active, -> { where(active: true) } + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :engaged, -> { where("status = '#{STATUS_APPROVED}' OR status = '#{STATUS_REQUESTED}' OR status = '#{STATUS_SUSPENDED}'") } + + def before_validation + if self.booked_price.nil? + self.booked_price = compute_price + end + end + + def after_create + if card_presumed_ok && !sent_notices + send_notices + end + end + + def before_save + automatically_default_slot + end + + def around_update + + @default_slot_did_change = self.default_slot_id_changed? + + yield + + sync_lessons + sync_remaining_test_drives + + @default_slot_did_change = nil + @accepting = nil + @countering = nil + end + + # here for shopping_cart + def product_info + {price: booked_price, real_price: booked_price, total_price: booked_price} + end + # here for shopping_cart + def price + booked_price + end + + + def alt_slot + found = nil + lesson_booking_slots.each do |slot| + if slot.id != default_slot.id + found = slot + break + end + end + found + end + + def student + user + end + + def next_lesson + if recurring + session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + LessonSession.find(session.id) if session + else + lesson_sessions[0] + end + end + + def accept(lesson_session, slot, accepter) + if !is_active? + self.accepting = true + end + + self.active = true + self.status = STATUS_APPROVED + self.counter_slot = nil + self.default_slot = slot + self.accepter = accepter + + success = self.save + + if !success + puts "unable to accept lesson booking #{errors.inspect}" + end + success + end + + def counter(lesson_session, proposer, slot) + self.countering = true + self.lesson_booking_slots << slot + self.counter_slot = slot + #self.status = STATUS_COUNTERED + self.save + end + + def automatically_default_slot + if is_requested? + if lesson_booking_slots.length > 0 + self.default_slot = lesson_booking_slots[0] + end + end + end + + def sync_remaining_test_drives + if is_test_drive? || is_single_free? + if card_presumed_ok && !user_decremented + self.user_decremented = true + self.save(validate: false) + if is_single_free? + user.remaining_free_lessons = user.remaining_free_lessons - 1 + elsif is_test_drive? + user.remaining_test_drives = user.remaining_test_drives - 1 + end + user.save(validate: false) + end + end + end + + def create_minimum_booking_time + # trying to be too smart + #(Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + Time.now + end + + def sync_lessons + + if is_canceled? + # don't create new sessions if cancelled + return + end + + + if @default_slot_did_change + + end + # Here we go; let's create a lesson(s) as needed + + # we need to make lessons into the future a bit, to give time for everyone involved + minimum_start_time = create_minimum_booking_time + + # get all sessions that are already scheduled for this booking ahead of the minimum time + sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).order(:created_at) + + if @default_slot_did_change + # # adjust all session times + + offset = 0 + sessions.each_with_index do |item, i| + item.lesson_session.slot = default_slot + result = item.lesson_session.update_next_available_time(offset) + if result + offset = result + offset += 1 + end + end + end + + needed_sessions = determine_needed_sessions(sessions) + + # if the latest scheduled session is after the minimum start time, then bump up minimum start time + last_session = sessions.last + last_session.reload if last_session # because of @default_slot_did_change logic above, this can be necessary + + if last_session && last_session.scheduled_start && last_session.scheduled_start > minimum_start_time + minimum_start_time = last_session.scheduled_start + end + times = default_slot.scheduled_times(needed_sessions, minimum_start_time) + + scheduled_lessons(times) + end + + # sensitive to current time + def predicted_times_for_month(year, month) + first_day = Date.new(year, month, 1) + last_day = Date.new(year, month, -1) + sessions = MusicSession.joins(:lesson_session).where("lesson_sessions.lesson_booking_id = ?", id).where("scheduled_start >= ?", first_day).where("scheduled_start <= ?", last_day).order(:created_at) + + times = [] + + sessions.each do |session| + times << session.scheduled_start + end + last_session = sessions.last + + start_day = first_day + if last_session + start_day = last_session.scheduled_start.to_date + 1 + end + + # now flesh out the rest of the month with predicted times + + more_times = default_slot.scheduled_times(5, start_day) + + more_times.each do |time| + if time.to_date >= first_day && time.to_date <= last_day + times << time + end + end + times + end + + def determine_needed_sessions(sessions) + needed_sessions = 0 + if is_requested? + # in the case of a requested booking (not approved) only make one, even if it's recurring. This is for UI considerations + if sessions.count == 0 + needed_sessions = 1 + end + elsif is_active? + expected_num_sessions = recurring ? 2 : 1 + needed_sessions = expected_num_sessions - sessions.count + end + needed_sessions + end + + def scheduled_lessons(times) + times.each do |time| + + lesson_session = LessonSession.create(self) + + if lesson_session.errors.any? + puts "JamClass lesson session creation errors #{lesson_session.errors.inspect}" + @@log.error("JamClass lesson session creation errors #{lesson_session.errors.inspect}") + raise ActiveRecord::Rollback + end + ms_tz = ActiveSupport::TimeZone.new(default_slot.timezone) + ms_tz = "#{ms_tz.name},#{default_slot.timezone}" + + rsvps = [{instrument_id: 'other', proficiency_level: 0, approve: true}] + + music_session = MusicSession.create(student, { + name: "#{display_type2} JamClass taught by #{teacher.name}", + description: "This is a #{lesson_length}-minute #{display_type2} lesson with #{teacher.name}.", + musician_access: false, + fan_access: false, + genres: ['other'], + approval_required: false, + fan_chat: false, + legal_policy: "standard", + language: 'eng', + duration: lesson_length, + recurring_mode: false, + timezone: ms_tz, + create_type: MusicSession::CREATE_TYPE_LESSON, + is_unstructured_rsvp: true, + scheduled_start: time, + invitations: [teacher.id], + lesson_session: lesson_session, + rsvp_slots: rsvps + }) + + if music_session.errors.any? + puts "JamClass lesson scheduling errors #{music_session.errors.inspect}" + @@log.error("JamClass lesson scheduling errors #{music_session.errors.inspect}") + raise ActiveRecord::Rollback + end + + if lesson_session.is_active? + # send out email to student to act as something they can add to their calendar + Notification.send_student_jamclass_invitation(music_session, student) + end + + end + end + + def is_weekly_payment? + payment_style == PAYMENT_STYLE_WEEKLY + end + + def is_monthly_payment? + payment_style == PAYMENT_STYLE_MONTHLY + end + + def requires_per_session_billing? + is_normal? && !is_monthly_payment? + end + + def requires_teacher_distribution?(target) + if target.is_a?(JamRuby::LessonSession) + is_test_drive? || (is_normal? && !is_monthly_payment?) + elsif target.is_a?(JamRuby::LessonPackagePurchase) + is_monthly_payment? + else + raise "unable to determine object type of #{target}" + end + end + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_active? + active + end + + def validate_accepted + # accept is multipe purpose; either accept the initial request, or a counter slot + if self.status_was != STATUS_REQUESTED && counter_slot.nil? # && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This lesson is already #{self.status}.") + end + + self.accepting = false + end + + def send_notices + UserMailer.student_lesson_request(self).deliver + UserMailer.teacher_lesson_request(self).deliver + Notification.send_lesson_message('requested', lesson_sessions[0], false) # TODO: this isn't quite an 'accept' + self.sent_notices = true + self.save + end + + def lesson_package_type + if is_single_free? + LessonPackageType.single_free + elsif is_test_drive? + LessonPackageType.test_drive + elsif is_normal? + LessonPackageType.single + end + end + + def display_type2 + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + "Single" + end + end + + def display_type + if is_single_free? + "Free" + elsif is_test_drive? + "TestDrive" + elsif is_normal? + if recurring + "recurring" + else + "single" + end + + end + end + + # determine the price of this booking based on what the user wants, and the teacher's pricing + + def compute_price + if is_single_free? + 0 + elsif is_test_drive? + LessonPackageType.test_drive.price + elsif is_normal? + teacher.teacher.booking_price(lesson_length, payment_style != PAYMENT_STYLE_MONTHLY) + end + end + + def distribution_price_in_cents + if is_single_free? + 0 + elsif is_test_drive? + 10 * 100 + elsif is_normal? + booked_price * 100 + end + end + + def is_single_free? + lesson_type == LESSON_TYPE_FREE + end + + def is_test_drive? + lesson_type == LESSON_TYPE_TEST_DRIVE + end + + def is_normal? + lesson_type == LESSON_TYPE_PAID + end + + def dayWeekDesc(slot = default_slot) + day = case slot.day_of_week + when 0 then "Sunday" + when 1 then "Monday" + when 2 then "Tuesday" + when 3 then "Wednesday" + when 4 then "Thursday" + when 5 then "Friday" + when 6 then "Saturday" + end + + + if slot.hour > 11 + hour = slot.hour - 12 + if hour == 0 + hour = 12 + end + am_pm = 'pm' + else + hour = slot.hour + if hour == 0 + hour = 12 + end + am_pm = 'am' + end + + "#{day} at #{hour}:#{slot.minute}#{am_pm}" + + end + + def approved_before? + !self.accepter_id.nil? + end + def cancel(canceler, other, message) + + self.active = false + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + success = save + if success + if approved_before? + # just tell both people it's cancelled, to act as confirmation + Notification.send_lesson_message('canceled', next_lesson, false) + Notification.send_lesson_message('canceled', next_lesson, true) + UserMailer.student_lesson_booking_canceled(self, message).deliver + UserMailer.teacher_lesson_booking_canceled(self, message).deliver + chat_message_prefix = "Lesson Canceled" + else + if canceler == student + # if it's the first time acceptance student canceling, we call it a 'cancel' + Notification.send_lesson_message('canceled', next_lesson, false) + UserMailer.teacher_lesson_booking_canceled(self, message).deliver + chat_message_prefix = "Lesson Canceled" + else + # if it's the first time acceptance teacher, it was declined + UserMailer.student_lesson_booking_declined(self, message).deliver + Notification.send_lesson_message('declined', next_lesson, true) + chat_message_prefix = "Lesson Declined" + end + end + + chat_message = message.nil? ? chat_message_prefix : "#{chat_message_prefix}: #{message}" + msg = ChatMessage.create(canceler, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, other, self) + + end + + success + end + + def card_approved + self.card_presumed_ok = true + if self.save && !sent_notices + send_notices + end + end + + def validate_user + if card_presumed_ok && is_single_free? + if !user.has_free_lessons? + errors.add(:user, 'have no remaining free lessons') + end + + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + elsif is_test_drive? + if user.has_requested_test_drive?(teacher) && !user.admin + errors.add(:user, "has a requested TestDrive with this teacher") + end + if !user.has_test_drives? && !user.can_buy_test_drive? + errors.add(:user, "have no remaining test drives") + end + elsif is_normal? + #if !user.has_stored_credit_card? + # errors.add(:user, 'has no credit card stored') + #end + end + end + + def validate_teacher + # shouldn't we check if the teacher already has a booking in this time slot, or at least warn the user + end + + def validate_recurring + if is_single_free? || is_test_drive? + if recurring + errors.add(:recurring, "can not be true for this type of lesson") + end + end + + false + end + + def validate_lesson_booking_slots + if lesson_booking_slots.length == 0 || lesson_booking_slots.length == 1 + errors.add(:lesson_booking_slots, "must have two times specified") + end + end + + def validate_lesson_length + if is_single_free? || is_test_drive? + if lesson_length != 30 + errors.add(:lesson_length, "must be 30 minutes") + end + end + end + + def validate_payment_style + if is_normal? + if payment_style.nil? + errors.add(:payment_style, "can't be blank") + end + end + end + + + def self.book_free(user, teacher, lesson_booking_slots, description) + self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) + end + + def self.book_test_drive(user, teacher, lesson_booking_slots, description) + self.book(user, teacher, LessonBooking::LESSON_TYPE_TEST_DRIVE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description) + end + + def self.book_normal(user, teacher, lesson_booking_slots, description, recurring, payment_style, lesson_length) + self.book(user, teacher, LessonBooking::LESSON_TYPE_PAID, lesson_booking_slots, recurring, lesson_length, payment_style, description) + end + + def self.book(user, teacher, lesson_type, lesson_booking_slots, recurring, lesson_length, payment_style, description) + + lesson_booking = nil + LessonBooking.transaction do + + lesson_booking = LessonBooking.new + lesson_booking.user = user + lesson_booking.card_presumed_ok = user.has_stored_credit_card? + lesson_booking.sent_notices = false + lesson_booking.teacher = teacher + lesson_booking.lesson_type = lesson_type + lesson_booking.recurring = recurring + lesson_booking.lesson_length = lesson_length + lesson_booking.payment_style = payment_style + lesson_booking.description = description + lesson_booking.status = STATUS_REQUESTED + + # two-way association slots, for before_validation loic in slot to work + lesson_booking.lesson_booking_slots = lesson_booking_slots + lesson_booking_slots.each do |slot| + slot.lesson_booking = lesson_booking + slot.message = description + end if lesson_booking_slots + + if lesson_booking.save + msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking) + end + end + lesson_booking + end + + def self.unprocessed(current_user) + LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false) + end + + def self.requested(current_user) + LessonBooking.where(user_id: current_user.id).where(status: STATUS_REQUESTED) + end + + + def self.find_bookings_needing_sessions(minimum_start_time) + MusicSession.select([:lesson_booking_id]).joins(:lesson_session => :lesson_booking).where("lesson_bookings.active = true").where('lesson_bookings.recurring = true').where("scheduled_start is not null").where("scheduled_start > ?", minimum_start_time).group(:lesson_booking_id).having('count(lesson_booking_id) < 2') + end + + # 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 + bill_monthlies + end + + def self.bill_monthlies + now = Time.now + billable_monthlies(now).each do |lesson_booking| + lesson_booking.bill_monthly(now) + end + + today = now.to_date + seven_days_in_future = today + 7 + + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + next_month = seven_days_in_future.to_time + billable_monthlies(next_month).each do |lesson_booking| + lesson_booking.bill_monthly(next_month) + end + end + end + + def self.billable_monthlies(now) + current_month_first_day = Date.new(now.year, now.month, 1) + current_month_last_day = Date.new(now.year, now.month, -1) + #next_month_last_day = now.month == 12 ? Date.new(now.year + 1, 1, -1) : Date.new(now.year, now.month + 1, -1) + + LessonBooking + .joins(:lesson_sessions => :music_session) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND (lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}))") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .active + .where('music_sessions.scheduled_start >= ?', current_month_first_day) + .where('music_sessions.scheduled_start <= ?', current_month_last_day).uniq + +=begin + today = now.to_date + seven_days_in_future = today + 7 + + is_different_month = seven_days_in_future.month != today.month + if is_different_month + condition = "(((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) ) + OR ((lesson_package_purchases.year = #{seven_days_in_future.year} AND lesson_package_purchases.month = #{seven_days_in_future.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{seven_days_in_future.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{seven_days_in_future.month} ) ) )" + else + condition = "((lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) AND ( (EXTRACT(YEAR FROM lesson_sessions.created_at)) = #{current_month_first_day.year} AND (EXTRACT(MONTH FROM lesson_sessions.created_at)) = #{current_month_first_day.month} ) )" + end + + + # .where("(lesson_package_purchases.year = #{current_month_first_day.year} AND lesson_package_purchases.month = #{current_month_first_day.month}) OR (lesson_package_purchases.year = #{next_month_last_day.year} AND lesson_package_purchases.month = #{next_month_last_day.month})") + + # find any monthly-billed bookings that have a session coming up within 7 days, and if so, attempt to bill them + LessonBooking + .joins(:lesson_sessions) + .joins("LEFT JOIN lesson_package_purchases ON (lesson_package_purchases.lesson_booking_id = lesson_bookings.id AND #{condition})") + .where("lesson_package_purchases.id IS NULL OR (lesson_package_purchases.id IS NOT NULL AND lesson_package_purchases.post_processed = false)") + .where(payment_style: PAYMENT_STYLE_MONTHLY) + .where(status: STATUS_APPROVED) + .where('lesson_sessions.created_at >= ?', current_month_first_day) + .where('lesson_sessions.created_at <= ?', seven_days_in_future).uniq + +=end + end + + def self.bookings(student, teacher, since_at = nil) + bookings = LessonBooking.where(user_id: student.id, teacher_id: teacher.id) + + if since_at + bookings = bookings.where('created_at >= ?', since_at) + end + + bookings + end + + def self.engaged_bookings(student, teacher, since_at = nil) + bookings = bookings(student, teacher, since_at) + bookings.engaged + end + + def bill_monthly(now) + LessonBooking.transaction do + self.lock! + + current_month = Date.new(now.year, now.month, 1) + + bill_for_month(current_month) + + today = now.to_date + seven_days_in_future = today + 7 + is_different_month = seven_days_in_future.month != today.month + if is_different_month + bill_for_month(seven_days_in_future) + end + end + end + + def bill_for_month(day_in_month) + # try to find lesson package purchase for this month, and last month, and see if they need processing + current_month_purchase = lesson_package_purchases.where(lesson_booking_id: self.id, user_id: student.id, year: day_in_month.year, month: day_in_month.month).first + if current_month_purchase.nil? + current_month_purchase = LessonPackagePurchase.create(user, self, lesson_package_type, day_in_month.year, day_in_month.month) + end + current_month_purchase.bill_monthly + end + + def suspend! + # when this is called, the calling code sends out a email to let the student and teacher know (it feels unnatural it's not here, though) + self.status = STATUS_SUSPENDED + self.active = false + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).suspend! + end + end + end + + def unsuspend! + if self.status == STATUS_SUSPENDED + self.status = STATUS_APPROVED + self.active = true + if self.save + future_sessions.each do |lesson_session| + LessonSession.find(lesson_session.id).unsuspend! + end + end + end + end + + def future_sessions + lesson_sessions.joins(:music_session).where('scheduled_start > ?', Time.now).order(:created_at) + end + + def self.schedule_upcoming_lessons + minimum_start_time = (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60*60) + + lesson_bookings = find_bookings_needing_sessions(minimum_start_time) + + lesson_bookings.each do |data| + lesson_booking = LessonBooking.find(data["lesson_booking_id"]) + lesson_booking.sync_lessons + end + end + + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_bookings/" + id + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb new file mode 100644 index 000000000..758bbc319 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb @@ -0,0 +1,239 @@ +# represenst the type of lesson package +module JamRuby + class LessonBookingSlot < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:message, :accept_message, :cancel_message] + + @@log = Logging.logger[LessonBookingSlot] + + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :proposer, class_name: "JamRuby::User" + has_one :defaulted_booking, class_name: "JamRuby::LessonBooking", foreign_key: :default_slot_id, inverse_of: :default_slot + has_one :countered_booking, class_name: "JamRuby::LessonBooking", foreign_key: :counter_slot_id, inverse_of: :counter_slot + has_one :countered_lesson, class_name: "JamRuby::LessonSession", foreign_key: :counter_slot_id, inverse_of: :counter_slot + + SLOT_TYPE_SINGLE = 'single' + SLOT_TYPE_RECURRING = 'recurring' + + SLOT_TYPES = [SLOT_TYPE_SINGLE, SLOT_TYPE_RECURRING] + + validates :proposer, presence: true + validates :slot_type, inclusion: {in: SLOT_TYPES} + #validates :preferred_day + validates :day_of_week, numericality: {only_integer: true}, allow_blank: true # 0 = sunday - 6 = saturday + validates :hour, numericality: {only_integer: true} + validates :minute, numericality: {only_integer: true} + validates :timezone, presence: true # example: 'America/New_York' + validates :update_all, inclusion: {in: [true, false]} + + validate :validate_slot_type + validate :validate_slot_minimum_time, on: :create + validate :validate_proposer + before_validation :before_validation + + def before_validation + if proposer.nil? + self.proposer = container.student + end + end + + def container + if lesson_booking + lesson_booking + else + lesson_session + end + end + + def is_teacher_created? + self.proposer == container.teacher + end + + def is_student_created? + !is_teacher_created? + end + + def is_teacher_approved? + !is_teacher_created? + end + + def recipient + if is_teacher_created? + container.student + else + container.teacher + end + end + + def create_minimum_booking_time + (Time.now + APP_CONFIG.minimum_lesson_booking_hrs * 60 * 60) + end + + def scheduled_times(needed_sessions, minimum_start_time) + + times = [] + week_offset = 0 + + needed_sessions.times do |i| + candidate = scheduled_time(i + week_offset) + + if day_of_week && candidate <= minimum_start_time + # move it up a week + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + # sanity check + if candidate <= minimum_start_time + week_offset += 1 + candidate = scheduled_time(i + week_offset) + + if candidate <= minimum_start_time + raise "candidate time less than minimum start time even after scoot: #{lesson_booking.id} #{self.id}" + end + end + end + times << candidate + end + + times + end + + def next_day + date = Date.today + date += ((day_of_week - date.wday) % 7).abs + end + + # weeks is the number of weeks in the future to compute the time for + def scheduled_time(weeks) + + # get the timezone of the slot, so we can compute times + tz = TZInfo::Timezone.get(timezone) + + if preferred_day + time = tz.local_to_utc(Time.new(preferred_day.year, preferred_day.month, preferred_day.day, hour, minute, 0)) + else + adjusted = next_day + (weeks * 7) + # day of the week adjustment + time = tz.local_to_utc(Time.new(adjusted.year, adjusted.month, adjusted.day, hour, minute, 0)) + end + + time + end + + def lesson_length + safe_lesson_booking.lesson_length + end + + def safe_lesson_booking + found = lesson_booking + found ||= lesson_session.lesson_booking + end + + def pretty_scheduled_start(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{tz}" + else + "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" + end + end + + def pretty_start_time(with_timezone = true) + + start_time = scheduled_time(0) + + begin + tz = TZInfo::Timezone.get(timezone) + rescue Exception => e + @@log.error("unable to find timezone=#{tz_identifier}, e=#{e}") + end + + if tz + begin + start_time = tz.utc_to_local(start_time) + rescue Exception => e + @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") + puts "unable to convert #{e}" + end + end + + duration = lesson_length * 60 # convert from minutes to seconds + end_time = start_time + duration + if with_timezone + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip} #{tz.name}" + else + "#{start_time.strftime("%a, %b %e")} at #{start_time.strftime("%l:%M%P").strip}" + end + end + + def validate_proposer + if proposer && (proposer != container.student && proposer != container.teacher) + errors.add(:proposer, "must be either the student or teacher") + end + end + + def validate_slot_type + if slot_type == SLOT_TYPE_SINGLE + if preferred_day.nil? + errors.add(:preferred_day, "must be specified") + end + end + + if slot_type == SLOT_TYPE_RECURRING + if day_of_week.nil? + errors.add(:day_of_week, "must be specified") + end + end + end + + def validate_slot_minimum_time + + # this code will fail miserably if the slot is malformed + if errors.any? + return + end + + if is_teacher_created? + return # the thinking is that a teacher can propose much tighter to the time; since they only counter; maybe they talked to the student + end + + # + # minimum_start_time = create_minimum_booking_time + minimum_start_time = Time.now + + if day_of_week + # this is recurring; it will sort itself out + else + time = scheduled_time(0) + + if time <= minimum_start_time + #errors.add(:base, "must be at least #{APP_CONFIG.minimum_lesson_booking_hrs} hours in the future") + errors.add(:preferred_day, "can not be in the past") + end + end + + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb new file mode 100644 index 000000000..7d0e98e4d --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb @@ -0,0 +1,147 @@ +# represents the purchase of a LessonPackage +module JamRuby + class LessonPackagePurchase < ActiveRecord::Base + + @@log = Logging.logger[LessonPackagePurchase] + + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge + + # who purchased the lesson package? + belongs_to :user, class_name: "JamRuby::User", :foreign_key => "user_id", inverse_of: :lesson_purchases + belongs_to :lesson_package_type, class_name: "JamRuby::LessonPackageType" + belongs_to :teacher, class_name: "JamRuby::User" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + + has_one :sale_line_item, class_name: "JamRuby::SaleLineItem" + + validates :user, presence: true + validates :lesson_package_type, presence: true + validates :price, presence: true + validate :validate_test_drive, on: :create + + after_create :add_test_drives + after_create :create_charge + + def validate_test_drive + if user + 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 + + def create_charge + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_package_purchase = self + lesson_payment_charge.save! + end + + def add_test_drives + if self.lesson_package_type.is_test_drive? + new_test_drives = user.remaining_test_drives + 4 + User.where(id: user.id).update_all(remaining_test_drives: new_test_drives) + user.remaining_test_drives = new_test_drives + end + + end + + + def name + lesson_package_type.sale_display + end + + def amount_charged + lesson_payment_charge.amount_in_cents / 100.0 + end + + def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil) + purchase = LessonPackagePurchase.new + purchase.user = user + purchase.lesson_booking = lesson_booking + purchase.teacher = lesson_booking.teacher if lesson_booking + + if year + purchase.year = year + purchase.month = month + purchase.recurring = true + + if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) + purchase.teacher_distribution = TeacherDistribution.create_for_lesson_package_purchase(purchase) + end + else + purchase.recurring = false + end + + if lesson_booking + purchase.lesson_package_type = lesson_booking.lesson_package_type + purchase.price = lesson_booking.booked_price # lesson_package_type.booked_price(lesson_booking) + else + purchase.lesson_package_type = lesson_package_type + purchase.price = lesson_package_type.price + end + + purchase.save + purchase + end + + def price_in_cents + (price * 100).to_i + end + + def description(lesson_booking) + lesson_package_type.description(lesson_booking) + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + + def month_name + if recurring + Date.new(year, month, 1).strftime('%B') + else + 'non-monthly paid lesson' + end + end + + def student + user + end + + + def bill_monthly(force = false) + lesson_payment_charge.charge(force) + + if lesson_payment_charge.billed + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def is_card_declined? + billed == false && billing_error_reason == 'card_declined' + end + + def is_card_expired? + billed == false && billing_error_reason == 'card_expired' + end + + def last_billed_at_date + last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at + end + + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + end +end + diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb new file mode 100644 index 000000000..d60aaa14a --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -0,0 +1,106 @@ +# represenst the type of lesson package +module JamRuby + class LessonPackageType < ActiveRecord::Base + + @@log = Logging.logger[LessonPackageType] + + PRODUCT_TYPE = 'LessonPackageType' + + SINGLE_FREE = 'single-free' + TEST_DRIVE = 'test-drive' + SINGLE = 'single' + + LESSON_PACKAGE_TYPES = + [ + SINGLE_FREE, + TEST_DRIVE, + SINGLE + ] + + validates :name, presence: true + validates :description, presence: true + validates :price, presence: true + validates :package_type, presence: true, inclusion: {in: LESSON_PACKAGE_TYPES} + + def self.monthly + LessonPackageType.find(MONTHLY) + end + + def self.single_free + LessonPackageType.find(SINGLE_FREE) + end + + def self.test_drive + LessonPackageType.find(TEST_DRIVE) + end + + def self.single + LessonPackageType.find(SINGLE) + end + + def booked_price(lesson_booking) + if is_single_free? + 0 + elsif is_test_drive? + LessonPackageType.test_drive.price + elsif is_normal? + lesson_booking.booked_price #teacher.teacher.booking_price(lesson_booking.lesson_length, lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_SINGLE) + end + end + + def description(lesson_booking) + if is_single_free? + "Single Free Lesson" + elsif is_test_drive? + "Test Drive" + elsif is_normal? + if lesson_booking.recurring + "Recurring #{lesson_booking.payment_style == LessonBooking::PAYMENT_STYLE_WEEKLY ? "Weekly" : "Monthly"} #{lesson_booking.lesson_length}m" + else + "Single #{lesson_booking.lesson_length}m lesson" + end + end + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def is_single_free? + id == SINGLE_FREE + end + + def is_test_drive? + id == TEST_DRIVE + end + + def is_normal? + id == SINGLE + end + + + def sale_display + name + end + + def plan_code + if package_type == SINGLE_FREE + "lesson-package-single-free" + elsif package_type == TEST_DRIVE + "lesson-package-test-drive" + elsif package_type == SINGLE + "lesson-package-single" + else + raise "unknown lesson package type #{package_type}" + end + end + + def sales_region + 'Worldwide' + end + + def to_s + sale_display + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb new file mode 100644 index 000000000..f68e84fc6 --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb @@ -0,0 +1,89 @@ +module JamRuby + class LessonPaymentCharge < Charge + + has_one :lesson_session, class_name: "JamRuby::LessonSession", foreign_key: :charge_id + has_one :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase", foreign_key: :charge_id + + def max_retries + 5 + end + + def charged_user + @charged_user ||= target.student + end + + def resolve_target + if is_lesson? + lesson_session + else + lesson_package_purchase + end + end + def target + @target ||= resolve_target + end + + def lesson_booking + @lesson_booking ||= target.lesson_booking + end + + def student + charged_user + end + + def is_lesson? + !lesson_session.nil? + end + + def do_charge(force) + + if is_lesson? + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, lesson_session) + else + result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, nil, lesson_package_purchase, force) + lesson_booking.unsuspend! if lesson_booking.is_suspended? + end + + stripe_charge = result[:stripe_charge] + + self.amount_in_cents = stripe_charge.amount + self.save(validate: false) + + # update teacher distribution, because it's now ready to be given to them! + + distribution = target.teacher_distribution + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end + + stripe_charge + end + + def do_send_notices + if is_lesson? + UserMailer.student_lesson_normal_done(lesson_session).deliver + UserMailer.teacher_lesson_normal_done(lesson_session).deliver + else + UserMailer.student_lesson_monthly_charged(lesson_package_purchase).deliver + UserMailer.teacher_lesson_monthly_charged(lesson_package_purchase).deliver + end + end + + def do_send_unable_charge + if is_lesson? + UserMailer.student_unable_charge(lesson_session) + else + if !billing_should_retry + lesson_booking.suspend! + end + + UserMailer.student_unable_charge_monthly(lesson_package_purchase) + if lesson_booking.is_suspended? + # let the teacher know that we are having problems collecting from the student + UserMailer.teacher_unable_charge_monthly(lesson_package_purchase) + end + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb new file mode 100644 index 000000000..dec9512fd --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -0,0 +1,642 @@ +# represenst the type of lesson package +module JamRuby + class LessonSession < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:cancel_message] + + attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling + + + @@log = Logging.logger[LessonSession] + + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, to: :lesson_booking + delegate :pretty_scheduled_start, to: :music_session + + + STATUS_REQUESTED = 'requested' + STATUS_CANCELED = 'canceled' + STATUS_MISSED = 'missed' + STATUS_COMPLETED = 'completed' + STATUS_APPROVED = 'approved' + STATUS_SUSPENDED = 'suspended' + STATUS_COUNTERED = 'countered' + + STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED, STATUS_APPROVED, STATUS_SUSPENDED, STATUS_COUNTERED] + + LESSON_TYPE_SINGLE = 'paid' + LESSON_TYPE_SINGLE_FREE = 'single-free' + LESSON_TYPE_TEST_DRIVE = 'test-drive' + LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE] + + has_one :music_session, class_name: "JamRuby::MusicSession" + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons + belongs_to :canceler, class_name: "JamRuby::User", foreign_key: :canceler_id + belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" + belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id + belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + + + validates :duration, presence: true, numericality: {only_integer: true} + validates :lesson_booking, presence: true + validates :lesson_type, inclusion: {in: LESSON_TYPES} + validates :booked_price, presence: true + validates :status, presence: true, inclusion: {in: STATUS_TYPES} + validates :teacher_complete, inclusion: {in: [true, false]} + validates :student_complete, inclusion: {in: [true, false]} + validates :teacher_canceled, inclusion: {in: [true, false]} + validates :student_canceled, inclusion: {in: [true, false]} + validates :success, inclusion: {in: [true, false]} + validates :sent_notices, inclusion: {in: [true, false]} + validates :post_processed, inclusion: {in: [true, false]} + + validate :validate_creating, :if => :creating + validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling + + after_save :after_counter, :if => :countering + after_save :manage_slot_changes + after_create :create_charge + + scope :approved, -> { where(status: STATUS_APPROVED) } + scope :requested, -> { where(status: STATUS_REQUESTED) } + scope :canceled, -> { where(status: STATUS_CANCELED) } + scope :suspended, -> { where(status: STATUS_SUSPENDED) } + scope :completed, -> { where(status: STATUS_COMPLETED) } + scope :missed, -> { where(status: STATUS_MISSED) } + + def create_charge + if !is_test_drive? + self.lesson_payment_charge = LessonPaymentCharge.new + lesson_payment_charge.amount_in_cents = 0 + lesson_payment_charge.fee_in_cents = 0 + lesson_payment_charge.lesson_session = self + lesson_payment_charge.save! + end + end + + def manage_slot_changes + # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . + # TODO: what to do, what to do. + + end + + def suspend! + self.status = STATUS_SUSPENDED + self.save + end + + def unsuspend! + self.status = STATUS_APPROVED + self.save + end + + def self.hourly_check + analyse_sessions + complete_sessions + end + + def self.analyse_sessions + MusicSession.joins(lesson_session: :lesson_booking).where('session_removed_at IS NOT NULL').where('analysed = false').each do |music_session| + lession_session = music_session.lesson_session + lession_session.analyse + end + end + + def self.complete_sessions + MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('session_removed_at IS NOT NULL').where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| + lession_session = music_session.lesson_session + lession_session.session_completed + end + end + + def analyse + if self.analysed + return + end + + analysis = LessonSessionAnalyser.analyse(self) + + self.analysis = analysis_to_json(analysis) + self.success = analysis[:bill] + self.analysed_at = Time.now + self.analysed = true + + if lesson_booking.requires_teacher_distribution?(self) + self.teacher_distribution = TeacherDistribution.create_for_lesson(self) + end + + if self.save + # send out emails appropriate for this type of session + session_completed + end + end + + + def amount_charged + lesson_payment_charge.amount_in_cents / 100.0 + end + + def analysis_to_json(analysis) + json = {} + + analysis.each do |k, v| + if v.is_a?(Array) + array = [] + v.each do |item| + if item.is_a?(Range) + array << {begin: item.begin, end: item.end} + else + raise "expected range" + end + end + json[k] = array + else + json[k] = v + end + end + json.to_json + end + + def session_completed + LessonSession.transaction do + self.lock! + + if post_processed + # nothing to do. because this is an async job, it's possible this was queued up with another session_completed fired + return + end + + if lesson_booking.is_test_drive? + test_drive_completed + elsif lesson_booking.is_normal? + if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment? + recurring_completed + else + normal_lesson_completed + end + end + + end + end + + def bill_lesson + + lesson_payment_charge.charge + + if lesson_payment_charge.billed + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + + def test_drive_completed + + distribution = teacher_distribution + if distribution # not all lessons/payment charges have a distribution + distribution.ready = true + distribution.save(validate: false) + end + + if !sent_notices + if success + student.test_drive_succeeded(self) + else + student.test_drive_failed(self) + end + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + + def recurring_completed + if success + if lesson_booking.is_monthly_payment? + # monthly payments are handled at beginning of month; just poke with email, and move on + + if !sent_notices + # not in spec; just poke user and tell them we saw it was successfully completed + UserMailer.monthly_recurring_done(self).deliver + + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + else + bill_lesson + end + else + if lesson_booking.is_monthly_payment? + # bad session; just poke user + if !sent_notices + UserMailer.monthly_recurring_no_bill(self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + else + if !sent_notices + # bad session; just poke user + UserMailer.student_weekly_recurring_no_bill(student, self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + + end + end + end + + def normal_lesson_completed + if success + bill_lesson + else + if !sent_notices + UserMailer.student_lesson_normal_no_bill(self).deliver + UserMailer.teacher_lesson_no_bill(self).deliver + self.sent_notices = true + self.sent_notices_at = Time.now + self.post_processed = true + self.post_processed_at = Time.now + self.save(:validate => false) + end + end + end + + def after_counter + send_counter(@countered_lesson, @countered_slot) + end + + def scheduled_start + music_session.scheduled_start + end + + def send_counter(countered_lesson, countered_slot) + if countered_slot.is_teacher_created? + UserMailer.student_lesson_counter(countered_lesson, countered_slot).deliver + else + UserMailer.teacher_lesson_counter(countered_lesson, countered_slot).deliver + end + self.countering = false + end + + default_scope { order('lesson_sessions.created_at') } + + def is_requested? + status == STATUS_REQUESTED + end + + def is_canceled? + status == STATUS_CANCELED + end + + def is_completed? + status == STATUS_COMPLETED + end + + def is_missed? + status == STATUS_MISSED + end + + def is_approved? + status == STATUS_APPROVED + end + + def is_suspended? + status == STATUS_SUSPENDED + end + + def is_countered? + status == STATUS_COUNTERED + end + + def validate_creating + if !is_requested? && !is_approved? + self.errors.add(:status, "is not valid for a new lesson session.") + end + + if is_approved? && lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + def validate_accepted + if self.status_was != STATUS_REQUESTED && self.status_was != STATUS_COUNTERED + self.errors.add(:status, "This session is already #{self.status_was}.") + end + + if approved_before? + # only checking for this on 1st time through acceptance + if lesson_booking && lesson_booking.is_test_drive? && lesson_package_purchase.nil? + self.errors.add(:lesson_package_purchase, "must be specified for a test drive purchase") + end + end + + self.accepting = false + end + + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + # check 24 hour window + if scheduled_start.to_i - Time.now.to_i < 24 * 60 * 60 + self.errors.add(:base, "This session is due to start within 24 hours and can not be canceled.") + end + + self.canceling = false + end + + def self.create(booking) + lesson_session = LessonSession.new + lesson_session.creating = true + lesson_session.duration = booking.lesson_length + lesson_session.lesson_type = booking.lesson_type + lesson_session.lesson_booking = booking + lesson_session.booked_price = booking.booked_price + lesson_session.teacher = booking.teacher + lesson_session.status = booking.status + lesson_session.slot = booking.default_slot + if booking.is_test_drive? + lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase + end + lesson_session.save + + if lesson_session.errors.any? + puts "Lesson Session errors #{lesson_session.errors.inspect}" + end + lesson_session + end + + def student + music_session.creator + end + + def self.index(user, params = {}) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = LessonSession.joins(:music_session).joins(music_session: :creator) + query = query.includes([:teacher, :music_session]) + query = query.order('music_sessions.scheduled_start DESC') + + if params[:as_teacher] + query = query.where('lesson_sessions.teacher_id = ?', user.id) + else + query = query.where('music_sessions.user_id = ?', user.id) + end + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + + def update_scheduled_start(week_offset) + music_session.scheduled_start = slot.scheduled_time(week_offset) + music_session.save! + end + + # grabs the next available time that's after the present, to avoid times being scheduled in the past + def update_next_available_time(attempt = 0) + max_attempts = attempt + 10 + while attempt < max_attempts + test = slot.scheduled_time(attempt) + + if test >= Time.now + time = test + # valid time found! + break + end + attempt += 1 + end + + if time + music_session.scheduled_start = time + music_session.save + end + + time.nil? ? nil : attempt + end + + # teacher accepts the lesson + def accept(params) + response = self + LessonSession.transaction do + + message = params[:message] + slot = params[:slot] + accepter = params[:accepter] + self.slot = slot = LessonBookingSlot.find(slot) + self.slot.accept_message = message + self.slot.save! + self.accepting = true + self.status = STATUS_APPROVED + + if !approved_before? + # 1st time this has ever been approved; there are other things we need to do + + if lesson_package_purchase.nil? && lesson_booking.is_test_drive? + self.lesson_package_purchase = student.most_recent_test_drive_purchase + end + + if self.save + # also let the lesson_booking know we got accepted + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end + UserMailer.student_lesson_accepted(self, message, slot).deliver + UserMailer.teacher_lesson_accepted(self, message, slot).deliver + chat_message = message ? "Lesson Approved: '#{message}'" : "Lesson Approved" + msg = ChatMessage.create(teacher, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking) + Notification.send_jamclass_invitation_teacher(music_session, teacher) + Notification.send_student_jamclass_invitation(music_session, student) + Notification.send_lesson_message('accept', self, true) + + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id}") + puts("unable to accept slot #{slot.id} for lesson #{self.id}") + response = self + raise ActiveRecord::Rollback + end + else + # this implies a new slot has been countered, and now approved + + if self.save + if slot.update_all + if !lesson_booking.accept(self, slot, accepter) + response = lesson_booking + raise ActiveRecord::Rollback + end + chat_message = message ? "All Lesson Times Updated: '#{message}'" : "All Lesson Times Updated" + msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept' + UserMailer.student_lesson_update_all(self, message, slot).deliver + UserMailer.teacher_lesson_update_all(self, message, slot).deliver + else + # nothing to do with the original booking (since we are not changing all times), so we update just ourself + time = update_next_available_time # XXX: week offset as 0? This *could* still be in the past. But the user is approving it. So do we just trust them and get out of their way? + + if time.nil? + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") + puts("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") + raise ActiveRecord::Rollback + end + chat_message = message ? "Lesson Updated Time Approved: '#{message}'" : "Lesson Updated Time Approved" + msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + UserMailer.student_lesson_accepted(self, message, slot).deliver + UserMailer.teacher_lesson_accepted(self, message, slot).deliver + end + else + @@log.error("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + puts("unable to accept slot #{slot.id} for lesson #{self.id} #{errors.inspect}") + response = self + raise ActiveRecord::Rollback + end + end + end + response + end + + def counter(params) + response = self + LessonSession.transaction do + proposer = params[:proposer] + slot = params[:slot] + message = params[:message] + + update_all = slot.update_all || !lesson_booking.recurring + self.countering = true + slot.proposer = proposer + slot.lesson_session = self + slot.message = message + self.lesson_booking_slots << slot + self.countered_slot = slot + self.countered_lesson = self + self.status = STATUS_COUNTERED + if !update_all + self.counter_slot = slot + end + if self.save + if update_all + if !lesson_booking.counter(self, proposer, slot) + response = lesson_booking + raise ActiveRecord::Rollback + end + end + else + response = self + raise ActiveRecord::Rollback + end + + + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + Notification.send_lesson_message('counter', self, slot.is_teacher_created?) + end + + response + end + + + # teacher accepts the lesson + def cancel(params) + response = self + LessonSession.transaction do + + canceler = params[:canceler] + other = canceler == teacher ? student : teacher + message = params[:message] + + if params[:update_all].present? + update_all = params[:update_all] + else + update_all = !lesson_booking.recurring + end + + + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + self.canceling = true + + if self.save + if update_all + if !lesson_booking.cancel(canceler, other, message) + response = lesson_booking + raise ActiveRecord::Rollback + end + else + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, lesson_booking) + Notification.send_lesson_message('canceled', self, false) + Notification.send_lesson_message('canceled', self, true) + UserMailer.student_lesson_canceled(self, message).deliver + UserMailer.teacher_lesson_canceled(self, message).deliver + end + else + response = self + raise ActiveRecord::Rollback + end + + end + + response + end + + def description(lesson_booking) + lesson_booking.lesson_package_type.description(lesson_booking) + end + + def stripe_description(lesson_booking) + description(lesson_booking) + end + + def home_url + APP_CONFIG.external_root_url + "/client#/jamclass" + end + + def web_url + APP_CONFIG.external_root_url + "/client#/jamclass/lesson-booking/" + id + end + + def update_payment_url + APP_CONFIG.external_root_url + "/client#/jamclass/update-payment" + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + id + end + end +end diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb new file mode 100644 index 000000000..d1883a77d --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -0,0 +1,307 @@ +module JamRuby + class LessonSessionAnalyser + + SUCCESS = 'success' + SESSION_ONGOING = 'session_ongoing' + THRESHOLD_MET = 'threshold_met' + WAITED_CORRECTLY = 'waited_correctly' + MINIMUM_TIME_MET = 'minimum_time_met' # for a teacher primarily; they waited around for the student sufficiently + MINIMUM_TIME_NOT_MET = 'mininum_time_not_met' + LATE_CANCELLATION = 'late_cancellation' + + TEACHER_FAULT = 'teacher_fault' + STUDENT_FAULT = 'student_fault' + BOTH_FAULT = 'both_fault' + + STUDENT_NOT_THERE_WHEN_JOINED = 'student_not_there_when_joined' + JOINED_LATE = 'did_not_join_on_time' + NO_SHOW = 'no_show' + + + # what are the potential results? + + # bill: true/false + + # teacher: 'no_show' + # teacher: 'late' + # teacher: 'early_leave' + # teacher: 'waited_correctly' + # teacher: 'late_cancellation' + + # student: 'no_show' + # student: 'late' + # student: 'early_leave' + # student: 'minimum_time_not_met' + # student: 'threshold_met' + + + # reason: 'session_ongoing' + # reason: 'success' + # reason: 'student_fault' + # reason: 'teacher_fault' + # reason: 'both_fault' + + + def self.analyse(lesson_session) + reason = nil + teacher = nil + student = nil + bill = false + + music_session = lesson_session.music_session + + student_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.student.id) + teacher_histories = MusicSessionUserHistory.where(music_session_id: music_session.id, user_id: lesson_session.teacher.id) + + # create ranges from music session user history + all_student_ranges = time_ranges(student_histories) + all_teacher_ranges = time_ranges(teacher_histories) + + # flatten ranges into non-overlapping ranges to simplifly logic + student_ranges = merge_overlapping_ranges(all_student_ranges) + teacher_ranges = merge_overlapping_ranges(all_teacher_ranges) + + intersecting = intersecting_ranges(student_ranges, teacher_ranges) + + student_analysis = analyse_intersection(lesson_session, student_ranges) + teacher_analysis = analyse_intersection(lesson_session, teacher_ranges) + together_analysis = analyse_intersection(lesson_session, intersecting) + + # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest + + if music_session.session_removed_at.nil? + reason = SESSION_ONGOING + bill = false + else + if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? + # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. + teacher = LATE_CANCELLATION + bill = false + elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? + # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. + student = LATE_CANCELLATION + bill = true + elsif together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes + bill = true + reason = SUCCESS + elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] + # if the teacher was present in the session within the first 5 minutes of the scheduled start time and stayed in the session for 10 minutes; + # and if either: + + if student_analysis[:no_show] + # the student no-showed entirely, then we bill the student. + student = NO_SHOW + bill = true + + elsif student_analysis[:joined_late] + # the student joined the lesson more than 10 minutes after the teacher did, regardless of whether the teacher was still in the lesson session at that point; then we bill the student + student = JOINED_LATE + bill = true + end + end + + end + + if reason.nil? + if student + reason = STUDENT_FAULT + elsif teacher + reason = TEACHER_FAULT + end + end + + { + reason: reason, + teacher: teacher, + student: student, + bill: bill, + student_ranges: student_ranges, + teacher_ranges: teacher_ranges, + intersecting: intersecting, + student_analysis: student_analysis, + teacher_analysis: teacher_analysis, + together_analysis: together_analysis, + } + end + + def self.annotate_timeline(lesson_session, analysis, ranges) + start = lesson_session.scheduled_start + end + + def self.intersecting_ranges(ranges_a, ranges_b) + intersections = [] + ranges_a.each do |range_a| + ranges_b.each do |range_b| + intersection = intersect(range_a, range_b) + intersections << intersection if intersection + end + end + + merge_overlapping_ranges(intersections) + end + + # needs to add to joined_on_time + # joined_on_time bool + # waited_correctly bool + # no_show bool + # joined_late bool + # minimum_time_met bool + # present_at_end bool + + + def self.analyse_intersection(lesson_session, ranges) + start = lesson_session.scheduled_start + planned_duration_seconds = lesson_session.duration * 60 + end_time = start + planned_duration_seconds + + join_start_boundary_begin = start + join_start_boundary_end = start + (APP_CONFIG.lesson_join_time_window_minutes * 60) + + wait_boundary_begin = start + wait_boundary_end = start + (APP_CONFIG.lesson_wait_time_window_minutes * 60) + + initial_join_window = Range.new(join_start_boundary_begin, join_start_boundary_end) + initial_wait_window = Range.new(wait_boundary_begin, wait_boundary_end) + session_window = Range.new(start, end_time) + + # let's see how much time they spent together, irrespective of scheduled time + # and also, based on scheduled time + total = 0 + + in_scheduled_time = 0 + in_wait_window_time = 0 + + # the initial time joined in the initial 'waiting window' + initial_join_in_scheduled_time = nil + + # the amount of time spent in the initial 'waiting window' + initial_wait_time_in_scheduled_time = 0 + + last_wait_time_out = nil + + joined_on_time = false + waited_correctly = false + no_show = true + joined_late = false + joined_in_wait_window = false + ranges.each do |range| + time = range.end - range.begin + + total += time + + in_session_range = intersect(range, session_window) + in_join_window_range = intersect(range, initial_join_window) + in_wait_window_range = intersect(range, initial_wait_window) + + if in_session_range + in_scheduled_time += in_session_range.end - in_session_range.begin + no_show = false + end + + if in_join_window_range + if initial_join_in_scheduled_time.nil? + initial_join_in_scheduled_time = in_join_window_range.begin + end + joined_on_time = true + end + + if in_wait_window_range + in_wait_window_time += in_wait_window_range.end - in_wait_window_range.begin + last_wait_time_out = range.end + joined_in_wait_window = true + end + end + + if joined_in_wait_window && !joined_on_time + joined_late = true + end + + if last_wait_time_out && last_wait_time_out > wait_boundary_end + last_wait_time_out = wait_boundary_end + end + + initial_waiting_time_pct = nil + potential_waiting_time = nil + + # let's see if this person was hanging around for the bulk of this waiting window (to rule out someone coming/going very fast, trying to miss someone) + if last_wait_time_out && initial_join_in_scheduled_time + total_in_waiting_time = 0 + potential_waiting_range = Range.new(initial_join_in_scheduled_time, last_wait_time_out) + ranges.each do |range| + in_waiting = intersect(potential_waiting_range, range) + if in_waiting + total_in_waiting_time += in_waiting.end - in_waiting.begin + end + end + + potential_waiting_time = last_wait_time_out - initial_join_in_scheduled_time + initial_waiting_time_pct = total_in_waiting_time.to_f / potential_waiting_time.to_f + + # finally with all this stuff calculated, we can check: + # 1) did they wait a solid % of time between the time they joined, and left, during the initial 10 minute waiting window? + # 2) did they + if (initial_waiting_time_pct >= APP_CONFIG.wait_time_window_pct) && + (last_wait_time_out >= (wait_boundary_end - (APP_CONFIG.end_of_wait_window_forgiveness_minutes * 60))) + waited_correctly = true + end + end + + + # percentage computation of time spent during the session time + in_scheduled_percentage = in_scheduled_time.to_f / planned_duration_seconds.to_f + + joined_on_time = joined_on_time + { + total_time: total, + session_time: in_scheduled_time, + session_pct: in_scheduled_percentage, + joined_on_time: joined_on_time, + waited_correctly: waited_correctly, + no_show: no_show, + joined_late: joined_late, + initial_join_in_scheduled_time: initial_join_in_scheduled_time, + last_wait_time_out: last_wait_time_out, + in_wait_window_time: in_wait_window_time, + initial_waiting_pct: initial_waiting_time_pct, + potential_waiting_time: potential_waiting_time + } + end + + def self.intersect(a, b) + min, max = a.first, a.exclude_end? ? a.max : a.last + other_min, other_max = b.first, b.exclude_end? ? b.max : b.last + + new_min = a === other_min ? other_min : b === min ? min : nil + new_max = a === other_max ? other_max : b === max ? max : nil + + new_min && new_max ? Range.new(new_min, new_max) : nil + end + + def self.time_ranges(histories) + ranges = [] + histories.each do |history| + ranges << history.range + end + ranges + end + + def self.ranges_overlap?(a, b) + a.include?(b.begin) || b.include?(a.begin) + end + + def self.merge_ranges(a, b) + [a.begin, b.begin].min..[a.end, b.end].max + end + + def self.merge_overlapping_ranges(ranges) + ranges.sort_by(&:begin).inject([]) do |ranges, range| + if !ranges.empty? && ranges_overlap?(ranges.last, range) + ranges[0...-1] + [merge_ranges(ranges.last, range)] + else + ranges + [range] + end + end + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb new file mode 100644 index 000000000..068721a2b --- /dev/null +++ b/ruby/lib/jam_ruby/models/lesson_session_monthly_price.rb @@ -0,0 +1,28 @@ +module JamRuby + class LessonSessionMonthlyPrice + + # calculate the price for a given month + def self.price(lesson_booking, start_day) + + raise "lesson_booking is not monthly paid #{lesson_booking.admin_url}" if !lesson_booking.is_monthly_payment? + + times = lesson_booking.predicted_times_for_month(start_day.year, start_day.month) + + result = nil + if times.length == 0 + result = 0 + elsif times.length == 1 + result = (lesson_booking.booked_price * 0.25).round(2) + elsif times.length == 2 + result = (lesson_booking.booked_price * 0.50).round(2) + elsif times.length == 3 + result = (lesson_booking.booked_price * 0.75).round(2) + else + result = lesson_booking.booked_price + end + + result + end + end +end + diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 68c387d47..d63c798db 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -21,6 +21,7 @@ module JamRuby CREATE_TYPE_RSVP = 'rsvp' CREATE_TYPE_IMMEDIATE = 'immediately' CREATE_TYPE_QUICK_START = 'quick-start' + CREATE_TYPE_LESSON = 'lesson' attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed @@ -31,12 +32,10 @@ module JamRuby self.primary_key = 'id' belongs_to :creator,:class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :music_session_histories - belongs_to :band, :class_name => 'JamRuby::Band', :foreign_key => :band_id, :inverse_of => :music_sessions - belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id - belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession" has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id" @@ -190,6 +189,10 @@ module JamRuby end end + def is_lesson? + !!lesson_session + end + def grouped_tracks tracks = [] self.music_session_user_histories.each do |msuh| @@ -375,6 +378,11 @@ module JamRuby ms.create_type = options[:create_type] ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp] ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone] + ms.scheduled_start = options[:scheduled_start] if options[:scheduled_start] + if options[:lesson_session] + ms.lesson_session = options[:lesson_session] + end + ms.save @@ -414,6 +422,10 @@ module JamRuby case ms.create_type when CREATE_TYPE_RSVP, CREATE_TYPE_SCHEDULE_FUTURE Notification.send_scheduled_session_invitation(ms, receiver) + when CREATE_TYPE_LESSON + if ms.lesson_session.is_active? + Notification.send_jamclass_invitation_teacher(ms, receiver) + end else Notification.send_session_invitation(receiver, user, ms.id) end @@ -932,7 +944,7 @@ SQL # with_timezone = FALSE # Thursday, July 10 - 10:00pm # this should be in a helper - def pretty_scheduled_start(with_timezone) + def pretty_scheduled_start(with_timezone = true) if scheduled_start && scheduled_duration start_time = scheduled_start diff --git a/ruby/lib/jam_ruby/models/music_session_user_history.rb b/ruby/lib/jam_ruby/models/music_session_user_history.rb index c8b8c0834..2e7bd25d4 100644 --- a/ruby/lib/jam_ruby/models/music_session_user_history.rb +++ b/ruby/lib/jam_ruby/models/music_session_user_history.rb @@ -10,8 +10,8 @@ module JamRuby attr_accessible :max_concurrent_connections, :session_removed_at, :rating validates_inclusion_of :rating, :in => -1..1, :allow_nil => true - belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :music_session_user_histories - belongs_to :music_session, :class_name => "MusicSession", :foreign_key => "music_session_id" + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => :user_id, :inverse_of => :music_session_user_histories + belongs_to :music_session, :class_name => "MusicSession", :foreign_key => :music_session_id def self.latest_history(client_id) self.where(:client_id => client_id) @@ -25,6 +25,10 @@ module JamRuby user.name end + def range + Range.new(created_at, session_removed_at || Time.now) + end + def music_session @msh ||= JamRuby::MusicSession.find_by_music_session_id(self.music_session_id) end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 6a0821f40..d47ecff00 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -12,6 +12,7 @@ module JamRuby belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id" belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id" belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id" + belongs_to :lesson_session, :class_name => "JamRuby::LessonSession", :foreign_key => "lesson_session_id" belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id" belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id" @@ -62,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, :band => band, :session => session}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -132,6 +133,8 @@ module JamRuby user = options[:user] band = options[:band] session = options[:session] + purpose = options[:purpose] + student_directed = options[:student_directed] name, band_name = "" unless user.nil? @@ -249,8 +252,37 @@ module JamRuby when NotificationTypes::BAND_INVITATION_ACCEPTED return "#{name} has accepted your band invitation to join #{band_name}." + when NotificationTypes::LESSON_MESSAGE + notification_msg = 'Lesson Changed' + + if purpose == 'requested' + notification_msg = 'You have received a lesson request' + elsif purpose == 'accept' + notification_msg = 'Your lesson request is confirmed!' + elsif purpose == 'declined' + notification_msg = "We're sorry your lesson request has been declined." + elsif purpose == 'canceled' + notification_msg = "Your lesson request has been canceled." + elsif purpose == 'counter' + if student_directed + notification_msg = "Instructor has proposed a different time for your lesson." + else + notification_msg = "Student has proposed a different time for your lesson." + end + elsif purpose == 'reschedule' + 'A lesson reschedule has been requested' + end + return notification_msg + + when NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + if student_directed + "You have been scheduled to take a JamClass with #{user.name}." + else + "You have been scheduled to teach a JamClass to #{user.name}" + end + else - return "" + return description end end @@ -372,6 +404,49 @@ module JamRuby end end + def send_lesson_message(purpose, lesson_session, student_directed) + + notification = Notification.new + notification.description = NotificationTypes::LESSON_MESSAGE + notification.student_directed = student_directed + + if !student_directed + notification.source_user_id = lesson_session.student.id + notification.target_user_id = lesson_session.teacher.id + else + notification.source_user_id = lesson_session.teacher.id + notification.target_user_id = lesson_session.student.id + end + + notification.purpose = purpose + notification.session_id = lesson_session.music_session.id + notification.lesson_session_id = lesson_session.id + + notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose}) + + #notification.message = notification_msg + + notification.save + + # receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at + message = @@message_factory.lesson_message( + notification.target_user.id, + notification.source_user.resolved_photo_url, + notification.source_user.name, + notification.source_user.id, + notification_msg, + notification.id, + notification.session_id, + notification.created_date, + notification.student_directed, + notification.purpose, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(notification.target_user.id, message) + + end + def send_new_band_follower(follower, band) band.band_musicians.each.each do |bm| @@ -644,44 +719,129 @@ module JamRuby end end - def send_scheduled_session_invitation(music_session, user) - + def send_jamclass_invitation_teacher(music_session, user) return if music_session.nil? || user.nil? - target_user = user - source_user = music_session.creator + teacher = target_user = user + student = source_user = music_session.creator + + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: student, student_directed: false}) notification = Notification.new - notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION notification.source_user_id = source_user.id notification.target_user_id = target_user.id notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = false + #notification.message = notification_msg notification.save - notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) + if target_user.online - msg = @@message_factory.scheduled_session_invitation( - target_user.id, - music_session.id, - source_user.photo_url, - notification_msg, - music_session.name, - music_session.pretty_scheduled_start(false), - notification.id, - notification.created_date + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session.id ) @@mq_router.publish_to_user(target_user.id, msg) end begin - UserMailer.scheduled_session_invitation(target_user, notification_msg, music_session).deliver + UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver rescue => e - @@log.error("Unable to send SCHEDULED_SESSION_INVITATION email to user #{target_user.email} #{e}") + @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{music_session.lesson_session.teacher.email} #{e}") end end + def send_student_jamclass_invitation(music_session, user) + return if music_session.nil? || user.nil? + + student = target_user = user + teacher = source_user = music_session.lesson_session.teacher + notification_msg = format_msg(NotificationTypes::SCHEDULED_JAMCLASS_INVITATION, {user: teacher, student_directed: true}) + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_JAMCLASS_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.lesson_session_id = music_session.lesson_session.id + notification.student_directed = true + #notification.message = notification_msg + notification.save + + if target_user.online + msg = @@message_factory.scheduled_jamclass_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date, + notification.lesson_session_id + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{student.email} #{e}") + end + + end + + + def send_scheduled_session_invitation(music_session, user) + + return if music_session.nil? || user.nil? + + target_user = user + source_user = music_session.creator + + notification = Notification.new + notification.description = NotificationTypes::SCHEDULED_SESSION_INVITATION + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.save + + notification_msg = format_msg(notification.description, {:user => source_user, :session => music_session}) + + if target_user.online + msg = @@message_factory.scheduled_session_invitation( + target_user.id, + music_session.id, + source_user.photo_url, + notification_msg, + music_session.name, + music_session.pretty_scheduled_start(false), + notification.id, + notification.created_date + ) + + @@mq_router.publish_to_user(target_user.id, msg) + end + + begin + UserMailer.scheduled_session_invitation(target_user, notification_msg, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_INVITATION email to user #{target_user.email} #{e}") + end + end + def send_scheduled_session_rsvp(music_session, user, instruments) return if music_session.nil? || user.nil? diff --git a/ruby/lib/jam_ruby/models/review.rb b/ruby/lib/jam_ruby/models/review.rb index 7c98619d9..4fe6e755b 100644 --- a/ruby/lib/jam_ruby/models/review.rb +++ b/ruby/lib/jam_ruby/models/review.rb @@ -20,6 +20,17 @@ module JamRuby after_save :reduce + def self.create(params) + review = Review.new + review.target = params[:target] + review.user = params[:user] + review.rating = params[:rating] + review.description = params[:description] + review.target_type = params[:target].class.to_s + review.save + review + end + def self.index(options={}) if options.key?(:include_deleted) arel = Review.all diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 0490b1afa..5cbd4f9d7 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -4,6 +4,7 @@ module JamRuby class Sale < ActiveRecord::Base JAMTRACK_SALE = 'jamtrack' + LESSON_SALE = 'lesson' SOURCE_RECURLY = 'recurly' SOURCE_IOS = 'ios' @@ -35,12 +36,12 @@ module JamRuby # will_paginate gem query = query.paginate(:page => current_page, :per_page => limit) - if query.length == 0 # no more results - { query: query, next_page: nil} - elsif query.length < limit # no more results - { query: query, next_page: nil} + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} else - { query: query, next_page: next_page } + {query: query, next_page: next_page} end end @@ -207,6 +208,116 @@ module JamRuby free && non_free end + def self.purchase_test_drive(current_user, booking = nil) + self.purchase_lesson(current_user, booking, LessonPackageType.test_drive) + end + + def self.purchase_normal(current_user, booking) + self.purchase_lesson(current_user, booking, LessonPackageType.single, booking.lesson_sessions[0]) + end + + # this is easy to make generic, but right now, it just purchases lessons + def self.purchase_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false) + stripe_charge = nil + sale = nil + purchase = nil + # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it + Sale.transaction(:requires_new => true) do + + sale = create_lesson_sale(current_user) + + if sale.valid? + + sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + + price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force) + + if price_info[:purchase] && price_info[:purchase].errors.any? + purchase = price_info[:purchase] + raise ActiveRecord::Rollback + end + + if !sale_line_item.valid? + raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}" + end + # sale.source = 'stripe' + sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents] + sale.recurly_tax_in_cents = price_info[:tax_in_cents] + sale.recurly_total_in_cents = price_info[:total_in_cents] + sale.recurly_currency = price_info[:currency] + sale.stripe_charge_id = price_info[:charge_id] + sale.save + stripe_charge = price_info[:charge] + purchase = price_info[:purchase] + else + # should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point. + raise "invalid sale object" + end + + end + {sale: sale, stripe_charge: stripe_charge, purchase: purchase} + end + + def self.charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false) + if lesson_package_purchase + target = lesson_package_purchase + elsif lesson_session + target = lesson_session + else + target = lesson_package_type + end + + current_user.sync_stripe_customer + + purchase = lesson_package_purchase + purchase = LessonPackagePurchase.create(current_user, lesson_booking, lesson_package_type) if purchase.nil? + + if purchase.errors.any? + price_info = {} + price_info[:purchase] = purchase + return price_info + end + + if lesson_session + lesson_session.lesson_package_purchase_id = purchase.id + lesson_session.save! + end + + subtotal_in_cents = purchase.price_in_cents + + tax_percent = 0 + if current_user.stripe_zip_code + lookup = ZipCodes.identify(current_user.stripe_zip_code) + if lookup && lookup[:state_code] == 'TX' + tax_percent = 0.0825 + end + end + + tax_in_cents = (subtotal_in_cents * tax_percent).round + total_in_cents = subtotal_in_cents + tax_in_cents + + stripe_charge = Stripe::Charge.create( + :amount => total_in_cents, + :currency => "usd", + :customer => current_user.stripe_customer_id, + :description => target.stripe_description(lesson_booking) + ) + + sale_line_item.lesson_package_purchase = purchase + sale_line_item.save + + price_info = {} + price_info[:subtotal_in_cents] = subtotal_in_cents + price_info[:tax_in_cents] = tax_in_cents + price_info[:total_in_cents] = total_in_cents + price_info[:currency] = 'USD' + price_info[:charge_id] = stripe_charge.id + price_info[:charge] = stripe_charge + price_info[:purchase] = purchase + price_info + end + + # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned def self.order_jam_tracks(current_user, shopping_carts) @@ -365,7 +476,6 @@ module JamRuby end - if account # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack adjustments = shopping_cart.create_adjustment_attributes(current_user) @@ -481,6 +591,10 @@ module JamRuby sale_type == JAMTRACK_SALE end + def is_lesson_sale? + sale_type == LESSON_SALE + end + def self.create_jam_track_sale(user, sale_source=nil) sale = Sale.new sale.user = user @@ -491,6 +605,15 @@ module JamRuby sale end + def self.create_lesson_sale(user) + sale = Sale.new + sale.user = user + sale.sale_type = LESSON_SALE # gift cards and jam tracks are sold with this type of sale + sale.order_total = 0 + sale.save + sale + end + # this checks just jamtrack sales appropriately def self.check_integrity_of_jam_track_sales Sale.select([:total, :voided]).find_by_sql( diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index e105f966b..61e753bbe 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -5,15 +5,22 @@ module JamRuby JAMCLOUD = 'JamCloud' JAMTRACK = 'JamTrack' GIFTCARD = 'GiftCardType' + LESSON = 'LessonPackageType' belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' belongs_to :gift_card, class_name: 'JamRuby::GiftCard' + belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase' + + # deprecated; use affiliate_distribution !! belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + + has_many :affiliate_distributions, class_name: 'JamRuby::AffiliateDistribution' + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' - validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD]} + validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]} validates :unit_price, numericality: {only_integer: false} validates :quantity, numericality: {only_integer: true} validates :free, numericality: {only_integer: true} @@ -81,8 +88,28 @@ module JamRuby line_item end - def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) - product_info = shopping_cart.product_info + # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible + def self.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking) + teacher = lesson_booking.teacher if lesson_booking + shopping_cart = ShoppingCart.create(current_user, lesson_package_type, 1) + line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil, lesson_booking) + + if lesson_booking + # if the teacher came from an affiliate, this is our chance to account for that (the student's affiliate status was accounted for in create_from_shopping_cart) + referral_info = teacher.should_attribute_sale?(shopping_cart, lesson_booking) + + if referral_info + line_item.affiliate_distributions << AffiliateDistribution.create(teacher.affiliate_referral, referral_info[:fee_in_cents], line_item) + line_item.save! + end + end + + shopping_cart.destroy + line_item + end + + def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil) + product_info = shopping_cart.product_info(instance) sale.order_total = sale.order_total + product_info[:real_price] @@ -101,9 +128,10 @@ module JamRuby # determine if we need to associate this sale with a partner user = shopping_cart.user - referral_info = user.should_attribute_sale?(shopping_cart) + referral_info = user.should_attribute_sale?(shopping_cart, instance) if referral_info + sale_line_item.affiliate_distributions << AffiliateDistribution.create(user.affiliate_referral, referral_info[:fee_in_cents], sale_line_item) sale_line_item.affiliate_referral = user.affiliate_referral sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] end diff --git a/ruby/lib/jam_ruby/models/school.rb b/ruby/lib/jam_ruby/models/school.rb new file mode 100644 index 000000000..1c573d778 --- /dev/null +++ b/ruby/lib/jam_ruby/models/school.rb @@ -0,0 +1,107 @@ +module JamRuby + class School < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:name] + + + # the school will handle all communication with students when setting up a session + SCHEDULING_COMM_SCHOOL = 'school' + # the teacher will handle all communication with students when setting up a session + SCHEDULING_COMM_TEACHER = 'teacher' + SCHEDULING_COMMS = [ SCHEDULING_COMM_SCHOOL, SCHEDULING_COMM_TEACHER ] + + attr_accessor :updating_avatar + attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection + + belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_school + has_many :students, class_name: ::JamRuby::User + has_many :teachers, class_name: ::JamRuby::Teacher + has_many :school_invitations, class_name: 'JamRuby::SchoolInvitation' + + validates :user, presence: true + validates :enabled, inclusion: {in: [true, false]} + validates :scheduling_communication, inclusion: {in: SCHEDULING_COMMS} + validates :correspondence_email, email: true, allow_blank: true + validate :validate_avatar_info + + before_save :stringify_avatar_info, :if => :updating_avatar + + def update_from_params(params) + self.name = params[:name] if params[:name].present? + self.scheduling_communication = params[:scheduling_communication] if params[:scheduling_communication].present? + self.correspondence_email = params[:correspondence_email] if params[:correspondence_email].present? + self.save + end + + def owner + user + end + + def validate_avatar_info + if updating_avatar + # we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io) + errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil? + errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil? + errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil? + end + end + + def escape_filename(path) + dir = File.dirname(path) + file = File.basename(path) + "#{dir}/#{ERB::Util.url_encode(file)}" + end + + def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket) + self.updating_avatar = true + + cropped_s3_path = cropped_fpfile["key"] + cropped_large_s3_path = cropped_large_fpfile["key"] + + self.update_attributes( + :original_fpfile => original_fpfile, + :cropped_fpfile => cropped_fpfile, + :cropped_large_fpfile => cropped_large_fpfile, + :cropped_s3_path => cropped_s3_path, + :cropped_large_s3_path => cropped_large_s3_path, + :crop_selection => crop_selection, + :photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true), + :large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true) + ) + end + + def delete_avatar(aws_bucket) + + User.transaction do + + unless self.cropped_s3_path.nil? + S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg') + S3Util.delete(aws_bucket, self.cropped_s3_path) + S3Util.delete(aws_bucket, self.cropped_large_s3_path) + end + + return self.update_attributes( + :original_fpfile => nil, + :cropped_fpfile => nil, + :cropped_large_fpfile => nil, + :cropped_s3_path => nil, + :cropped_large_s3_path => nil, + :photo_url => nil, + :crop_selection => nil, + :large_photo_url => nil + ) + end + end + + def stringify_avatar_info + # fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR, + # so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object) + # later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable + # client parse it, because it's very rare when it's needed at all + self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? + self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? + self.crop_selection = crop_selection.to_json if !crop_selection.nil? + end + end +end diff --git a/ruby/lib/jam_ruby/models/school_invitation.rb b/ruby/lib/jam_ruby/models/school_invitation.rb new file mode 100644 index 000000000..beea9fd3c --- /dev/null +++ b/ruby/lib/jam_ruby/models/school_invitation.rb @@ -0,0 +1,100 @@ +module JamRuby + class SchoolInvitation < ActiveRecord::Base + + include HtmlSanitize + html_sanitize strict: [:note] + + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :school, class_name: ::JamRuby::School + + validates :school, presence: true + validates :email, email: true + validates :invitation_code, presence: true + validates :as_teacher, inclusion: {in: [true, false]} + validates :accepted, inclusion: {in: [true, false]} + validates :first_name, presence: true + validates :last_name, presence: true + validate :school_has_name, on: :create + + before_validation(on: :create) do + self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil? + end + + def school_has_name + if school && school.name.blank? + errors.add(:school, "must have name") + end + end + + def self.index(school, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = SchoolInvitation.where(school_id: school.id) + query = query.includes([:user, :school]) + query = query.order('created_at') + query = query.where(as_teacher: params[:as_teacher]) + query = query.where(accepted:false) + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + + + def self.create(current_user, specified_school, params) + + invitation = SchoolInvitation.new + invitation.school = specified_school + invitation.as_teacher = params[:as_teacher] + invitation.email = params[:email] + invitation.first_name = params[:first_name] + invitation.last_name = params[:last_name] + + if invitation.save + invitation.send_invitation + end + invitation + end + + + def send_invitation + if as_teacher + UserMailer.invite_school_teacher(self).deliver + else + UserMailer.invite_school_student(self).deliver + end + end + def generate_signup_url + if as_teacher + "#{APP_CONFIG.external_root_url}/school/#{school.id}/teacher?invitation_code=#{self.invitation_code}" + else + "#{APP_CONFIG.external_root_url}/school/#{school.id}/student?invitation_code=#{self.invitation_code}" + end + end + + def delete + self.destroy + end + + def resend + send_invitation + end + + + + end +end diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index fb531c2bd..5c3543f70 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -26,9 +26,13 @@ module JamRuby default_scope order('created_at DESC') - def product_info + def product_info(instance = nil) product = self.cart_product - {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil? + data = {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display} unless product.nil? + if data && instance + data.merge!(instance.product_info) + end + data end # multiply quantity by price @@ -115,6 +119,10 @@ module JamRuby cart_type == GiftCardType::PRODUCT_TYPE end + def is_lesson? + cart_type == LessonPackageType::PRODUCT_TYPE + end + # returns an array of adjustments for the shopping cart def create_adjustment_attributes(current_user) raise "not a jam track or gift card" unless is_jam_track? || is_gift_card? diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index c95353d6c..69640f35a 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -13,6 +13,16 @@ module JamRuby validates :redirect_location, length: {maximum: 1000} validates :want_jamblaster, inclusion: {in: [nil, true, false]} + def self.create_redirect(user, options = {}) + hint = SignupHint.new + hint.user = user + hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) + hint.want_jamblaster = false + hint.expires_at = 2.days.from_now + hint.save + hint + end + def self.refresh_by_anoymous_user(anonymous_user, options = {}) hint = SignupHint.find_by_anonymous_user_id(anonymous_user.id) @@ -34,5 +44,13 @@ module JamRuby SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all end + def self.most_recent_redirect(user, default) + hint = SignupHint.where(user_id: user.id).order('created_at desc').first + if hint + hint.redirect_location + else + default + end + end end end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index 0daca4373..a6982e944 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -13,8 +13,11 @@ module JamRuby has_many :experiences_education, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'education'} has_many :experiences_award, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'award'} has_many :reviews, :class_name => "JamRuby::Review", as: :target - has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target - has_one :user, :class_name => 'JamRuby::User' + has_many :lesson_sessions, :class_name => "JamRuby::LessonSession" + has_many :lesson_package_purchases, :class_name => "JamRuby::LessonPackagePurchase" + has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target + has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers validates :user, :presence => true validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction @@ -52,6 +55,9 @@ module JamRuby query = User.joins(:teacher) + # only show teachers with background check set and ready for session set to true + query = query.where('teachers.ready_for_session_at IS NOT NULL') + instruments = params[:instruments] if instruments && !instruments.blank? && instruments.length > 0 query = query.joins("inner JOIN teachers_instruments AS tinst ON tinst.teacher_id = teachers.id") @@ -87,7 +93,7 @@ module JamRuby end years_teaching = params[:years_teaching].to_i - if years_teaching && years_teaching > 0 + if params[:years_teaching] && years_teaching > 0 query = query.where('years_teaching >= ?', years_teaching) end @@ -95,20 +101,20 @@ module JamRuby teaches_intermediate = params[:teaches_intermediate] teaches_advanced = params[:teaches_advanced] - if teaches_beginner || teaches_intermediate || teaches_advanced + if teaches_beginner.present? || teaches_intermediate.present? || teaches_advanced.present? clause = '' - if teaches_beginner + if teaches_beginner == true clause << 'teaches_beginner = true' end - if teaches_intermediate + if teaches_intermediate == true if clause.length > 0 clause << ' OR ' end clause << 'teaches_intermediate = true' end - if teaches_advanced + if teaches_advanced == true if clause.length > 0 clause << ' OR ' end @@ -118,7 +124,7 @@ module JamRuby end student_age = params[:student_age].to_i - if student_age && student_age > 0 + if params[:student_age] && student_age > 0 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 @@ -150,13 +156,11 @@ module JamRuby end teacher = user.teacher - teacher ||= user.build_teacher() + teacher ||= Teacher.new teacher.user = user teacher.website = params[:website] if params.key?(:website) teacher.biography = params[:biography] if params.key?(:biography) - teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video) - teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video) teacher.years_teaching = params[:years_teaching] if params.key?(:years_teaching) teacher.years_playing = params[:years_playing] if params.key?(:years_playing) @@ -186,6 +190,7 @@ module JamRuby teacher.price_per_month_120_cents = params[:price_per_month_120_cents] if params.key?(:price_per_month_120_cents) teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive) teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) + teacher.school_id = params[:school_id] if params.key?(:school_id) # Many-to-many relations: if params.key?(:genres) @@ -243,6 +248,21 @@ module JamRuby teacher end + def booking_price(lesson_length, single) + price = nil + if single + price = self["price_per_lesson_#{lesson_length}_cents"] + else + price = self["price_per_month_#{lesson_length}_cents"] + end + + if !price.nil? + price / 100.0 + else + price + end + end + def offer_pricing unless prices_per_lesson.present? || prices_per_month.present? errors.add(:offer_pricing, "Must choose to price per lesson or per month") diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb new file mode 100644 index 000000000..585d8e61b --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -0,0 +1,76 @@ +module JamRuby + class TeacherDistribution < ActiveRecord::Base + + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: "teacher_id" + belongs_to :teacher_payment, class_name: "JamRuby::TeacherPayment" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" + + validates :teacher, presence: true + validates :amount_in_cents, presence: true + + def self.create_for_lesson(lesson_session) + distribution = create(lesson_session) + distribution.lesson_session = lesson_session + distribution + end + + def self.create_for_lesson_package_purchase(lesson_package_purchase) + distribution = create(lesson_package_purchase) + distribution.lesson_package_purchase = lesson_package_purchase + distribution + end + + def self.create(target) + distribution = TeacherDistribution.new + distribution.teacher = target.teacher + distribution.ready = false + distribution.distributed = false + distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents + distribution + end + def amount + amount_in_cents / 100.0 + end + + def student + if lesson_session + lesson_session.student + else + lesson_package_purchase.student + end + end + + def month_name + lesson_package_purchase.month_name + end + + def is_test_drive? + lesson_session && lesson_session.is_test_drive? + end + + def is_normal? + lesson_session && !lesson_session.is_test_drive? + end + + def is_monthly? + !lesson_package_purchase.nil? + end + + def description + if lesson_session + if lesson_session.lesson_booking.is_test_drive? + "Test Drive session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}" + elsif lesson_session.lesson_booking.is_normal? + if lesson_session.lesson_booking.is_weekly_payment? || lesson_session.lesson_booking.is_monthly_payment? + raise "Should not be here" + else + "A session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}" + end + end + else + "Monthly session for the month of #{lesson_package_purchase.month_name} with #{lesson_package_purchase.lesson_booking.student.name}" + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_intent.rb b/ruby/lib/jam_ruby/models/teacher_intent.rb new file mode 100644 index 000000000..333cf5afe --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_intent.rb @@ -0,0 +1,24 @@ +module JamRuby + class TeacherIntent < ActiveRecord::Base + + belongs_to :user, class_name: ::JamRuby::User + belongs_to :teacher, class_name: ::JamRuby::Teacher + + validates :user, presence: true + validates :teacher, presence: true + validates :intent, presence: true + + def self.create(user, teacher, intent) + teacher_intent = TeacherIntent.new + teacher_intent.user = user + teacher_intent.teacher = teacher + teacher_intent.intent = intent + teacher_intent.save + teacher_intent + end + + def self.recent_test_drive(user) + TeacherIntent.where(intent: 'book-test-drive').where(user_id: user.id).where('created_at > ?', Date.today - 30).order('created_at DESC').first + end + end +end diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb new file mode 100644 index 000000000..3ee7ee162 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment.rb @@ -0,0 +1,112 @@ +module JamRuby + class TeacherPayment < ActiveRecord::Base + + belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id + belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id + has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" + + + def self.hourly_check + teacher_payments + end + + def teacher_distributions + [teacher_distribution] + end + + def self.pending_teacher_payments + User.select(['users.id']).joins(:teacher).joins(:teacher_distributions).where('teachers.stripe_account_id IS NOT NULL').where('teacher_distributions.distributed = false').where('teacher_distributions.ready = true').uniq + end + + def self.teacher_payments + 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| + payment = TeacherPayment.charge(teacher) + if payment.nil? || !payment.teacher_payment_charge.billed + break + end + end + + end + end + + def amount + amount_in_cents / 100.0 + end + + def is_card_declined? + teacher_payment_charge.is_card_declined? + end + + def is_card_expired? + teacher_payment_charge.is_card_expired? + end + + def last_billed_at_date + teacher_payment_charge.last_billed_at_date + end + def charge_retry_hours + 24 + end + + def calculate_teacher_fee + if teacher_distribution.is_test_drive? + 0 + else + (amount_in_cents * 0.28).round + end + end + + + # will find, for a given teacher, an outstading unsuccessful payment or make a new one. + # it will then associate a charge with it, and then execute the charge. + def self.charge(teacher) + payment = TeacherPayment.joins(:teacher_payment_charge).where('teacher_payments.teacher_id = ?', teacher.id).where('charges.billed = false').order(:created_at).first + if payment.nil? + payment = TeacherPayment.new + payment.teacher = teacher + else + payment = TeacherPayment.find(payment.id) + end + + if payment.teacher_distribution.nil? + teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).order(:created_at).first + if teacher_distribution.nil? + return + end + payment.teacher_distribution = teacher_distribution + end + + + payment.amount_in_cents = payment.teacher_distribution.amount_in_cents + payment.fee_in_cents = payment.calculate_teacher_fee + + if payment.teacher_payment_charge.nil? + charge = TeacherPaymentCharge.new + charge.amount_in_cents = payment.amount_in_cents + charge.fee_in_cents = payment.fee_in_cents + charge.teacher_payment = payment + payment.teacher_payment_charge = charge + # charge.save! + else + charge = payment.teacher_payment_charge + charge.amount_in_cents = payment.amount_in_cents + 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 + payment + end + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb new file mode 100644 index 000000000..e54bc5330 --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb @@ -0,0 +1,53 @@ +module JamRuby + class TeacherPaymentCharge < Charge + + has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :charge_id + + def distribution + @distribution ||= teacher_payment.teacher_distribution + end + + def max_retries + 9999999 + end + + def teacher + @teacher ||= teacher_payment.teacher + end + + def charged_user + teacher + end + + + + def do_charge(force) + + # source will let you supply a token. But... how to get a token in this case? + + stripe_charge = Stripe::Charge.create( + :amount => amount_in_cents, + :currency => "usd", + :customer => APP_CONFIG.stripe[:source_customer], + :description => construct_description, + :destination => teacher.teacher.stripe_account_id, + :application_fee => fee_in_cents, + ) + + stripe_charge + end + + def do_send_notices + UserMailer.teacher_distribution_done(teacher_payment) + end + + def do_send_unable_charge + UserMailer.teacher_distribution_fail(teacher_payment) + end + + def construct_description + teacher_payment.teacher_distribution.description + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 50f350c01..bfd92fdae 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -170,6 +170,13 @@ module JamRuby has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight", :foreign_key => "user_id" has_many :purchased_jam_tracks, :through => :jam_track_rights, :class_name => "JamRuby::JamTrack", :source => :jam_track, :order => :created_at + # lessons + has_many :lesson_purchases, :class_name => "JamRuby::LessonPackagePurchase", :foreign_key => "user_id", inverse_of: :user + has_many :student_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "user_id", inverse_of: :user + has_many :teacher_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "teacher_id", inverse_of: :teacher + has_many :teacher_distributions, :class_name => "JamRuby::TeacherDistribution", :foreign_key => "teacher_id", inverse_of: :teacher + has_many :teacher_payments, :class_name => "JamRuby::TeacherPayment", :foreign_key => "teacher_id", inverse_of: :teacher + # Shopping carts has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart" @@ -187,10 +194,14 @@ module JamRuby has_one :musician_search, :class_name => 'JamRuby::MusicianSearch' has_one :band_search, :class_name => 'JamRuby::BandSearch' - belongs_to :teacher, :class_name => 'JamRuby::Teacher' + belongs_to :teacher, :class_name => 'JamRuby::Teacher', foreign_key: :teacher_id has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession" + has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id + belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students + has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user + has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser" has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users @@ -200,7 +211,6 @@ module JamRuby validates :first_name, length: {maximum: 50}, no_profanity: true validates :last_name, length: {maximum: 50}, no_profanity: true - validates :last_name, length: {maximum: 50}, no_profanity: true validates :biography, length: {maximum: 4000}, no_profanity: true validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email @@ -211,6 +221,8 @@ module JamRuby validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false} validates :reuse_card, :inclusion => {:in => [true, false]} + validates :is_a_student, :inclusion => {:in => [true, false]} + validates :is_a_teacher, :inclusion => {:in => [true, false]} validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} validates :gifted_jamtracks, presence: true, :numericality => {:less_than_or_equal_to => 100} validates :subscribe_email, :inclusion => {:in => [nil, true, false]} @@ -1076,12 +1088,25 @@ module JamRuby gift_card = options[:gift_card] student = options[:student] teacher = options[:teacher] + school_invitation_code = options[:school_invitation_code] + school_id = options[:school_id] user = User.new user.validate_instruments = true UserManager.active_record_transaction do |user_manager| - user.first_name = first_name - user.last_name = last_name + + if school_invitation_code + school_invitation = SchoolInvitation.find_by_invitation_code(school_invitation_code) + if school_invitation + first_name ||= school_invitation.first_name + last_name ||= school_invitation.last_name + school_invitation.accepted = true + school_invitation.save + end + end + + user.first_name = first_name if first_name.present? + user.last_name = last_name if last_name.present? user.email = email user.subscribe_email = true user.terms_of_service = terms_of_service @@ -1095,7 +1120,15 @@ module JamRuby end user.musician = !!musician - + if school_id.present? + if user.is_a_student + user.school_id = school_id + elsif user.is_a_teacher + school = School.find_by_id(school_id) + school_name = school ? school.name : 'a music school' + user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Teaches for #{school_name}", school_id: school_id) + end + end # FIXME: Setting random password for social network logins. This # is because we have validations all over the place on this. # The right thing would be to have this null @@ -1277,10 +1310,21 @@ module JamRuby user.save end if affiliate_referral_id.present? - # don't send an signup email if email is already confirmed - if user.email_confirmed + + + if user.is_a_student + UserMailer.student_welcome_message(user).deliver + end + + if user.is_a_teacher + UserMailer.teacher_welcome_message(user).deliver + end + + if !user.is_a_teacher && !user.is_a_student UserMailer.welcome_message(user).deliver - else + end + + if !user.email_confirmed # any errors here should also rollback the transaction; that's OK. If emails aren't going to be delivered, # it's already a really bad situation; make user signup again UserMailer.confirm_email(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token)).deliver @@ -1798,9 +1842,15 @@ module JamRuby options end - def should_attribute_sale?(shopping_cart) + def should_attribute_sale?(shopping_cart, instance = nil) + + if shopping_cart.is_lesson? && shopping_cart.cart_product.is_test_drive? + # never attribute test drives + return false + end + if affiliate_referral - referral_info = affiliate_referral.should_attribute_sale?(shopping_cart) + referral_info = affiliate_referral.should_attribute_sale?(shopping_cart, self, instance) else false end @@ -1821,11 +1871,215 @@ module JamRuby using_free_credit end + def has_stored_credit_card? + stored_credit_card + end + + def has_free_lessons? + remaining_free_lessons > 0 + end + + def can_book_free_lesson? + has_free_lessons? && has_stored_credit_card? + end + + def can_buy_test_drive? + lesson_purchases.where(lesson_package_type_id: LessonPackageType.test_drive.id).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0 + end + + def has_test_drives? + remaining_test_drives > 0 + end + + def has_unprocessed_test_drives? + !unprocessed_test_drive.nil? + end + + def has_requested_test_drive?(teacher = nil) + !requested_test_drive(teacher).nil? + end + + def fetch_stripe_customer + Stripe::Customer.retrieve(stripe_customer_id) + end + + # if the user already has a stripe customer, then keep it synced. otherwise create it + def sync_stripe_customer + if self.stripe_customer_id + # we already have a customer for this user; re-use it + customer = fetch_stripe_customer + + if customer.email.nil? || customer.email.downcase != email.downcase + customer.email = email + customer.save + end + else + customer = Stripe::Customer.create( + :description => admin_url, + :source => stripe_token, + :email => email) + end + self.stripe_customer_id = customer.id + User.where(id: id).update_all(stripe_customer_id: customer.id) + + customer + end + def card_approved(token, zip) + + approved_booking = nil + User.transaction do + self.stripe_token = token if token + self.stripe_zip_code = zip if zip + customer = sync_stripe_customer + self.stripe_customer_id = customer.id + self.stored_credit_card = true + if self.save + # we can also 'unlock' any booked sessions that still need to be done so + LessonBooking.unprocessed(self).each do |booking| + booking.card_approved + approved_booking = booking + end + end + end + approved_booking + end + + def update_name(name) + if name.blank? + self.first_name = '' + self.last_name = '' + else + bits = name.split + if bits.length == 1 + self.first_name = '' + self.last_name = bits[0].strip + elsif bits.length == 2 + self.first_name = bits[0].strip + self.last_name = bits[1].strip + else + self.first_name = bits[0].strip + self.last_name = bits[1..-1].join(' ') + end + end + self.save + end + + def payment_update(params) + booking = nil + test_drive = nil + normal = nil + intent = nil + purchase = nil + User.transaction do + + if params[:name].present? + if !self.update_name(params[:name]) + return nil + end + end + + booking = card_approved(params[:token], params[:zip]) + if params[:test_drive] + self.reload + result = Sale.purchase_test_drive(self, booking) + test_drive = result[:sale] + purchase = result[:purchase] + elsif params[:normal] + self.reload + end + + intent = TeacherIntent.recent_test_drive(self) + end + + {lesson: booking, test_drive: test_drive, intent:intent, purchase: purchase} + end + + def requested_test_drive(teacher = nil) + query = LessonBooking.requested(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE) + if teacher + query = query.where(teacher_id: teacher.id) + end + query.first + end + + def unprocessed_test_drive + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_TEST_DRIVE).first + end + + def unprocessed_normal_lesson + LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first + end + + def most_recent_test_drive_purchase + lesson_purchases.where(lesson_package_type_id: LessonPackageType.test_drive.id).order('created_at desc').first + end + + def test_drive_succeeded(lesson_session) + if self.remaining_test_drives <= 0 + UserMailer.student_test_drive_lesson_done(lesson_session).deliver + UserMailer.teacher_lesson_completed(lesson_session).deliver + else + UserMailer.student_test_drive_lesson_completed(lesson_session).deliver + UserMailer.teacher_lesson_completed(lesson_session).deliver + end + end + + def test_drive_failed(lesson_session) + # because we decrement test_drive credits as soon as you book, we need to bring it back now + self.remaining_test_drives = self.remaining_test_drives + 1 + self.save(validate: false) + + UserMailer.test_drive_no_bill(self, lesson_session).deliver + end + + def used_test_drives + 4 - remaining_test_drives + end + + def has_rated_teacher(teacher) + if teacher.is_a?(JamRuby::User) + teacher = teacher.teacher + end + Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s).count > 0 + end + + def has_rated_student(student) + Review.where(target_id: student.id).where(target_type: "JamRuby::User").count > 0 + end + + def teacher_profile_url + "#{APP_CONFIG.external_root_url}/client#/profile/teacher/#{id}" + end + + def ratings_url + "#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/teacher/#{id}" + end + + def student_ratings_url + "#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/#{id}" + end + + def self.search_url + "#{APP_CONFIG.external_root_url}/client#/jamclass/searchOptions" + end + + def recent_test_drive_teachers + User.select('distinct on (users.id) users.*').joins(taught_lessons: :music_session).where('lesson_sessions.lesson_type = ?', LessonSession::LESSON_TYPE_TEST_DRIVE).where('music_sessions.user_id = ?', id).where('lesson_sessions.created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago) + end + def mark_session_ready self.ready_for_session_at = Time.now self.save! end + def has_booked_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).count > 0 + end + + def has_booked_test_drive_with_student?(student, since_at = nil) + LessonBooking.engaged_bookings(student, self, since_at).test_drive.count > 0 + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/models/user_authorization.rb b/ruby/lib/jam_ruby/models/user_authorization.rb index f4c1523eb..9710e7082 100644 --- a/ruby/lib/jam_ruby/models/user_authorization.rb +++ b/ruby/lib/jam_ruby/models/user_authorization.rb @@ -12,6 +12,10 @@ module JamRuby validates_uniqueness_of :uid, scope: :provider # token, secret, token_expiration can be missing + def is_active? + token_expiration && token_expiration < Time.now + end + def self.refreshing_google_auth(user) auth = self.where(:user_id => user.id) .where(:provider => 'google_login') diff --git a/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb new file mode 100644 index 000000000..d9097d3c6 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/hourly_job.rb @@ -0,0 +1,18 @@ +module JamRuby + class HourlyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_hourly_job + @@log = Logging.logger[HourlyJob] + + def self.perform + @@log.debug("waking up") + + LessonBooking.hourly_check + LessonSession.hourly_check + TeacherPayment.hourly_check + + @@log.debug("done") + end + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 9e62c3500..73c0a9fad 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -21,6 +21,9 @@ FactoryGirl.define do reuse_card true has_redeemable_jamtrack true gifted_jamtracks 0 + remaining_free_lessons 1 + remaining_test_drives 0 + stored_credit_card false #u.association :musician_instrument, factory: :musician_instrument, user: u @@ -92,10 +95,17 @@ FactoryGirl.define do connection = FactoryGirl.create(:connection, :user => user, :music_session => active_music_session) end end + factory :teacher_user do + after(:create) do |user, evaluator| + teacher = FactoryGirl.create(:teacher, user: user, price_per_lesson_60_cents: 3000, price_per_month_60_cents: 3000) + end + end end factory :teacher, :class => JamRuby::Teacher do association :user, factory: :user + price_per_lesson_60_cents 3000 + price_per_month_60_cents 3000 end factory :musician_instrument, :class => JamRuby::MusicianInstrument do @@ -146,6 +156,9 @@ FactoryGirl.define do end factory :music_session, :class => JamRuby::MusicSession do + ignore do + student nil + end sequence(:name) { |n| "Music Session #{n}" } sequence(:description) { |n| "Music Session Description #{n}" } fan_chat true @@ -893,7 +906,6 @@ FactoryGirl.define do association :user, factory: :user sequence(:serial_no ) { |n| "serial_no#{n}" } - sequence(:vtoken ) { |n| "vtoken#{n}" } sequence(:client_id ) { |n| "client_id#{n}" } end @@ -906,6 +918,116 @@ FactoryGirl.define do sequence(:sibling_key ) { |n| "sibling_key#{n}" } end + factory :school, class: 'JamRuby::School' do + association :user, factory: :user + sequence(:name) {|n| "Dat Music School"} + enabled true + scheduling_communication 'teacher' + end + + factory :school_invitation, class: 'JamRuby::SchoolInvitation' do + association :school, factory: :school + note "hey come in in" + as_teacher true + sequence(:email) {|n| "school_person#{n}@example.com"} + sequence(:first_name) {|n| "FirstName"} + sequence(:last_name) {|n| "LastName"} + accepted false + end + + factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do + factory :lesson_booking_slot_single do + slot_type 'single' + preferred_day Date.today + 3 + day_of_week nil + hour 12 + minute 30 + timezone 'UTC' + end + + factory :lesson_booking_slot_recurring do + slot_type 'recurring' + preferred_day nil + day_of_week 0 + hour 12 + minute 30 + timezone 'UTC' + end + end + + factory :lesson_booking, class: 'JamRuby::LessonBooking' do + association :user, factory: :user + association :teacher, factory: :teacher_user + card_presumed_ok false + sent_notices false + recurring false + lesson_length 30 + lesson_type JamRuby::LessonBooking::LESSON_TYPE_FREE + payment_style JamRuby::LessonBooking::PAYMENT_STYLE_ELSEWHERE + description "Oh my goodness!" + status JamRuby::LessonBooking::STATUS_REQUESTED + before(:create) do |lesson_booking, evaluator| + lesson_booking.lesson_booking_slots = [FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking), + FactoryGirl.build(:lesson_booking_slot_single, lesson_booking: lesson_booking)] + end + #lesson_booking_slots [FactoryGirl.build(:lesson_booking_slot_single), FactoryGirl.build(:lesson_booking_slot_single)] + end + + factory :lesson_package_purchase, class: "JamRuby::LessonPackagePurchase" do + lesson_package_type { JamRuby::LessonPackageType.single } + association :user, factory: :user + association :teacher, factory: :teacher_user + price 30.00 + + factory :test_drive_purchase do + lesson_package_type { JamRuby::LessonPackageType.test_drive } + association :lesson_booking, factory: :lesson_booking + price 49.99 + end + end + + factory :lesson_session, class: 'JamRuby::LessonSession' do + + ignore do + student nil + end + + music_session {FactoryGirl.create(:music_session, creator: student)} + lesson_booking {FactoryGirl.create(:lesson_booking, user: student, teacher: teacher)} + association :teacher, factory: :teacher_user + lesson_type JamRuby::LessonSession::LESSON_TYPE_SINGLE + duration 30 + booked_price 49.99 + status JamRuby::LessonSession::STATUS_REQUESTED + #teacher_complete true + #student_complete true + end + + factory :charge, class: 'JamRuby::Charge' do + type 'JamRuby::Charge' + amount_in_cents 1000 + end + + factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do + type 'JamRuby::TeacherPaymentCharge' + end + + + factory :teacher_payment, class: 'JamRuby::TeacherPayment' do + association :teacher, factory: :teacher_user + association :teacher_payment_charge, factory: :teacher_payment_charge + amount_in_cents 1000 + end + + # you gotta pass either lesson_session or lesson_package_purchase for this to make sense + factory :teacher_distribution, class: 'JamRuby::TeacherDistribution' do + association :teacher, factory: :teacher_user + association :teacher_payment, factory: :teacher_payment + ready false + amount_in_cents 1000 + end + + factory :ip_blacklist, class: "JamRuby::IpBlacklist" do remote_ip '1.1.1.1' end diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index 694323832..b16efb2d0 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -568,6 +568,5 @@ describe ConnectionManager, no_transaction: true do # connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS) # connection.errors.size.should == 0 end - end diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb new file mode 100644 index 000000000..505f97d85 --- /dev/null +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -0,0 +1,215 @@ +require 'spec_helper' + +describe "Monthly Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + + after {Timecop.return} + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + it "works" do + + # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on + Timecop.travel(Date.new(2016, 3, 20)) + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Student has proposed a different time for your lesson." + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 4 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Your lesson request is confirmed!" + + # let user pay for it + LessonBooking.hourly_check + + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + teacher_distribution = lesson_purchase.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + teacher_distribution.reload + teacher_distribution.distributed.should be_true + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql 3000 + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + + lesson.amount_charged.should eql 0.0 + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be false + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 1 # one for student + end +end diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb new file mode 100644 index 000000000..24b85c2ad --- /dev/null +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -0,0 +1,414 @@ +require 'spec_helper' + +describe "Normal Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "stripe mocked" do + before { StripeMock.start + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + after { StripeMock.stop } + after {Timecop.return} + + it "bill failure" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql booking.default_slot + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + + StripeMock.prepare_card_error(:card_declined) + + lesson_session.lesson_payment_charge.billing_attempts.should eql 0 + user.lesson_purchases.length.should eql 0 + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 1 + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + LessonBooking.hourly_check + + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_false + teacher_distribution.distributed.should be_false + + + # let's reattempt right away; this should have no effect because we only try to bill once per 24 hours + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_error_reason.should eql 'card_declined' + lesson_session.billed.should eql false + lesson_session.billing_attempts.should eql 1 + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 1).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:expired_card) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 2 + lesson_session.billing_error_reason.should eql 'card_expired' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + Timecop.freeze((24 + 24 + 2).hours.from_now) + StripeMock.clear_errors + StripeMock.prepare_card_error(:processing_error) + + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 3 + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql false + user.reload + user.lesson_purchases.length.should eql 0 + + + Timecop.freeze((24 + 24 + 24 + 3).hours.from_now) + StripeMock.clear_errors + + # finally the user will get billed! + LessonSession.hourly_check + + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + + payment.amount_in_cents.should eql 3248 + payment.fee_in_cents.should eql 0 + + lesson_session.post_processed.should be_true + LessonPaymentCharge.count.should eql 2 + + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billing_attempts.should eql 4 + if lesson_session.billing_error_detail + lesson_session.billing_error_detail + end + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.billed.should eql true + user.reload + user.lesson_purchases.length.should eql 1 + + + LessonBooking.hourly_check + payment.reload + payment.amount_in_cents.should eql 3248 + payment.fee_in_cents.should eql 0 + lesson_session.reload + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 3000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.reload + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should eql 'processing_error' + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + TeacherPayment.count.should eql 1 + teacher_distribution.reload + teacher_distribution.distributed.should be_true + + payment = TeacherPayment.first + payment.amount_in_cents.should eql 3000 + payment.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher_payment_charge.amount_in_cents.should eql 3000 + payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round + payment.teacher.should eql teacher_user + payment.teacher_distribution.should eql teacher_distribution + + end + end + + + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should eql 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + end +end diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb new file mode 100644 index 000000000..3946eecdc --- /dev/null +++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe "Recurring Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_normal_lesson + booking.sent_notices.should be_false + booking.booked_price.should eql 30.00 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.amount_charged.should be 0.0 + lesson.reload + + user.reload + user.stripe_customer_id.should_not be nil + user.remaining_test_drives.should eql 0 + user.lesson_purchases.length.should eql 0 + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + #notification.message.should eql "Instructor has proposed a different time for your lesson." + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16, update_all: true) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + UserMailer.deliveries.each do |del| + # puts del.inspect + end + # get acceptance emails, as well as 'your stuff is accepted' + UserMailer.deliveries.length.should eql 6 + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 6 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billed.should be true + user.reload + user.lesson_purchases.length.should eql 1 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + lesson_purchase.price_in_cents.should eql 3000 + user.sales.length.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should eql (100 * booking.booked_price.to_f * 0.0825).round.to_i + sale.recurly_total_in_cents.should eql ((100 * booking.booked_price.to_f * 0.0825).round + 100 * booking.booked_price.to_f).to_i + sale.recurly_subtotal_in_cents.should eql (100 * booking.booked_price).to_i + sale.recurly_currency.should eql 'USD' + sale.stripe_charge_id.should_not be_nil + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + line_item.lesson_package_purchase.should eql lesson_purchase + lesson_purchase.sale_line_item.should eql line_item + lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_billing_notices.should be true + user.reload + user.remaining_test_drives.should eql 0 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + end +end diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb new file mode 100644 index 000000000..e9545cfbe --- /dev/null +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -0,0 +1,219 @@ +require 'spec_helper' + +describe "TestDrive Lesson Flow" do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 0) } + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + before { + teacher.stripe_account_id = stripe_account1_id + teacher.save! + } + + it "works" do + + # user has no test drives, no credit card on file, but attempts to book a lesson + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.card_presumed_ok.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + booking.sent_notices.should be_false + user.reload + user.remaining_test_drives.should eql 0 + + ########## Need validate their credit card + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.reload + user.remaining_test_drives.should eql 3 + booking = result[:lesson] + lesson = booking.lesson_sessions[0] + test_drive = result[:test_drive] + booking.errors.any?.should be_false + lesson.errors.any?.should be_false + test_drive.errors.any?.should be_false + booking.card_presumed_ok.should be_true + booking.sent_notices.should be_true + lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + lesson.reload + + + test_drive.stripe_charge_id.should_not be_nil + test_drive.recurly_tax_in_cents.should be 412 + test_drive.recurly_total_in_cents.should eql 4999 + 412 + test_drive.recurly_subtotal_in_cents.should eql 4999 + test_drive.recurly_currency.should eql 'USD' + line_item = test_drive.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive.id + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions.length.should eql 1 + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonBooking::STATUS_REQUESTED + booking.status.should eql LessonBooking::STATUS_REQUESTED + + ######### Teacher counters with new slot + teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14) + UserMailer.deliveries.clear + lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'}) + booking.reload + booking.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 1 + lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user + teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last + teacher_counter.should eql teacher_countered_slot + teacher_counter.proposer.should eql teacher_user + booking.lesson_booking_slots.length.should eql 3 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.message.should eql 'Does this work?' + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######### Student counters with new slot + student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + UserMailer.deliveries.clear + lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.lesson_booking.errors.any?.should be false + lesson_session.lesson_booking_slots.length.should eql 2 + student_counter = booking.lesson_booking_slots.order(:created_at).last + student_counter.proposer.should eql user + booking.reload + booking.lesson_booking_slots.length.should eql 4 + UserMailer.deliveries.length.should eql 1 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Does this work better?' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql user + chat.target_user.should eql teacher_user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql false + notification.purpose.should eql 'counter' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + + ######## Teacher accepts slot + UserMailer.deliveries.clear + + lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.reload + lesson_session.slot.should eql student_counter + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.default_slot.should eql student_counter + lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.status.should eql LessonBooking::STATUS_APPROVED + + UserMailer.deliveries.length.should eql 4 + chat = ChatMessage.unscoped.order(:created_at).last + chat.message.should eql 'Yeah I got this' + chat.channel.should eql ChatMessage::CHANNEL_LESSON + chat.user.should eql teacher_user + chat.target_user.should eql user + notification = Notification.unscoped.order(:created_at).last + notification.session_id.should eql lesson_session.music_session.id + notification.student_directed.should eql true + notification.purpose.should eql 'accept' + notification.description.should eql NotificationTypes::LESSON_MESSAGE + notification.message.should eql "Your lesson request is confirmed!" + + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + + + UserMailer.deliveries.clear + # background code comes around and analyses the session + LessonSession.hourly_check + lesson_session.reload + lesson_session.analysed.should be_true + analysis = JSON.parse(lesson_session.analysis) + analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis["student"].should eql LessonSessionAnalyser::NO_SHOW + lesson_session.billed.should be false + if lesson_session.billing_error_detail + puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests + end + lesson_session.billing_error_reason.should be_nil + lesson_session.sent_notices.should be true + purchase = lesson_session.lesson_package_purchase + purchase.should_not be nil + purchase.price_in_cents.should eql 4999 + purchase.lesson_package_type.is_test_drive?.should be true + user.reload + user.remaining_test_drives.should eql 3 + UserMailer.deliveries.length.should eql 2 # one for student, one for teacher + + teacher_distribution = lesson_session.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + LessonBooking.hourly_check + LessonSession.hourly_check + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_false + + TeacherPayment.count.should eql 0 + TeacherPayment.daily_check + TeacherPayment.count.should eql 1 + + lesson_session.reload + purchase.reload + + purchase.teacher_distribution.should be_nil + + teacher_payment = TeacherPayment.first + teacher_payment.amount_in_cents.should eql 1000 + teacher_payment.fee_in_cents.should eql 0 + teacher_payment.teacher.should eql teacher_user + + teacher_distribution.reload + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be_true + teacher_distribution.distributed.should be_true + + teacher_payment.teacher_payment_charge.amount_in_cents.should eql 1000 + teacher_payment.teacher_payment_charge.fee_in_cents.should eql 0 + + end +end diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 3c23bdc73..99fd536a5 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -343,6 +343,8 @@ describe AffiliatePartner do freebie_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) freebie_sale.affiliate_referral_fee_in_cents.should eq(0) freebie_sale.created_at = Date.new(2015, 1, 1) + freebie_sale.affiliate_distributions.first.created_at = freebie_sale.created_at + freebie_sale.affiliate_distributions.first.save! freebie_sale.save! @@ -363,6 +365,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -378,6 +382,9 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -393,6 +400,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -424,6 +433,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2014, 12, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -439,6 +450,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! real_sale_later = real_sale AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) @@ -455,6 +468,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 3, 31) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -466,6 +481,9 @@ describe AffiliatePartner do real_sale.affiliate_refunded_at = Date.new(2015, 3, 1) real_sale.affiliate_refunded = true real_sale.save! + real_sale.affiliate_distributions.first.affiliate_refunded_at = real_sale.affiliate_refunded_at + real_sale.affiliate_distributions.first.affiliate_refunded = real_sale.affiliate_refunded + real_sale.affiliate_distributions.first.save! AffiliatePartner.ensure_quarters_exist(2015, 0) AffiliatePartner.total_quarters(2015, 0) quarter = partner1.quarters.first @@ -485,6 +503,9 @@ describe AffiliatePartner do real_sale_later.affiliate_refunded_at = Date.new(2015, 7, 1) real_sale_later.affiliate_refunded = true real_sale_later.save! + real_sale_later.affiliate_distributions.first.affiliate_refunded_at = real_sale_later.affiliate_refunded_at + real_sale_later.affiliate_distributions.first.affiliate_refunded = real_sale_later.affiliate_refunded + real_sale_later.affiliate_distributions.first.save! AffiliatePartner.total_quarters(2015, 1) payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) payment.due_amount_in_cents.should eq(20) @@ -556,6 +577,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 4, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 1)) AffiliateQuarterlyPayment.count.should eq(3) @@ -600,6 +623,8 @@ describe AffiliatePartner do real_sale.affiliate_referral_fee_in_cents.should eq(20) real_sale.created_at = Date.new(2015, 1, 1) real_sale.save! + real_sale.affiliate_distributions.first.created_at = real_sale.created_at + real_sale.affiliate_distributions.first.save! AffiliatePartner.tally_up(Date.new(2015, 4, 2)) quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) diff --git a/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb new file mode 100644 index 000000000..fb1019b97 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_slot_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBookingSlot do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single, timezone: 'US/Pacific', hour: 12) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring, timezone: 'US/Pacific', hour: 12) } + + + before do + + end + + after do + Timecop.return + end + + describe "next_day" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + it "same day of week" do + + @now.wday.should eql 1 + # let's make it match + lesson_booking_slot_recurring1.day_of_week = @now.wday + + (@now.to_date).should eql lesson_booking_slot_recurring1.next_day + end + + it "day of week is tomorrow" do + + # make the slot be today + 1 (Tuesday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday + 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 1) + end + + it "day of week is yesterday" do + # make the slot be today + 1 (Sunday in this case) + lesson_booking_slot_recurring1.day_of_week = @now.wday - 1 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 6) + end + + it "day of week is two day ago" do + # make the slot be today + 1 (Saturday in this case) + lesson_booking_slot_recurring1.day_of_week = 6 + + lesson_booking_slot_recurring1.next_day.should eql(@now.to_date + 5) + end + end + + describe "scheduled_time" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time OK" do + time = lesson_booking_slot_single1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(0) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time OK 1 week out" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + time = lesson_booking_slot_recurring1.scheduled_time(1) + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end + + + describe "scheduled_times" do + before do + # Set Time.now to September 1, 2008 10:05:00 AM (at this instant). This is Monday + @now = Time.local(2008, 9, 1, 10, 5, 0) + Timecop.freeze(@now) + end + + it "creates a single time session OK" do + times = lesson_booking_slot_single1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql lesson_booking_slot_single1.preferred_day.year + time.month.should eql lesson_booking_slot_single1.preferred_day.month + time.day.should eql lesson_booking_slot_single1.preferred_day.day + time.hour.should be > lesson_booking_slot_single1.hour + time.min.should eql lesson_booking_slot_single1.minute + time.sec.should eql 0 + end + + + it "creates a recurring time session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + 3 + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 3 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates a recurring time session OK (minimum time hit)" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(1, @now.to_date + 2) + times.length.should eq 1 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + + it "creates 2 recurring times session OK" do + lesson_booking_slot_recurring1.day_of_week = @now.wday + times = lesson_booking_slot_recurring1.scheduled_times(2, @now.to_date + 2) + times.length.should eq 2 + time = times[0] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 7 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + time = times[1] + time.utc_offset.should eq 0 + time.year.should eql @now.year + time.month.should eql @now.month + time.day.should eql @now.day + 14 + time.hour.should be > lesson_booking_slot_recurring1.hour + time.min.should eql lesson_booking_slot_recurring1.minute + time.sec.should eql 0 + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb new file mode 100644 index 000000000..ac64a0f3a --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -0,0 +1,899 @@ +require 'spec_helper' + +# collissions with teacher's schedule? +describe LessonBooking do + + let(:user) { FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1) } + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "suspend!" do + it "should set status as well as update status of all associated lesson_sessions" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.lesson_sessions[0].accept({accepter: teacher_user, message: "got it", slot: booking.lesson_booking_slots[0].id}) + booking.reload + booking.active.should eql true + booking.suspend! + booking.errors.any?.should be false + booking.reload + booking.active.should eql false + booking.status.should eql LessonBooking::STATUS_SUSPENDED + booking.lesson_sessions.count.should eql 2 + booking.lesson_sessions.each do |lesson_session| + lesson_session.status.should eql LessonBooking::STATUS_SUSPENDED + end + end + end + + describe "bill_monthlies" do + after do + Timecop.return + end + + it "empty" do + LessonBooking.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 0 + end + + it "one" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should be_nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + # don't advance time, but nothing should happen because last_billing time hasn't elapsed + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql false + purchase.billed_at.should eql nil + purchase.billing_attempts.should eql 2 + purchase.post_processed.should eql false + purchase.post_processed_at.should be_nil + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql false + + user.card_approved(create_stripe_token, '78759') + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 3 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "advances to next month" do + user.card_approved(create_stripe_token, '78759') + user.save! + + day = Date.new(2016, 1, 20) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + LessonSession.count.should eql 2 + + purchase = LessonPackagePurchase.first + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + + day = Date.new(2016, 1, 27) + time = day.to_time + Timecop.freeze(time) + + LessonBooking.schedule_upcoming_lessons + LessonSession.count.should eql 3 + LessonPackagePurchase.count.should eql 1 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 2 + + purchase = LessonPackagePurchase.order(:month).last + purchase.month.should eql 2 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 1 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + end + + it "will suspend after enough tries" do + day = Date.new(2016, 1, 1) + time = day.to_time + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + LessonBooking.count.should eql 1 + LessonPackagePurchase.count.should eql 0 + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + + purchase = LessonPackagePurchase.first + purchase.billing_error_reason.should eql "stripe" + purchase.billing_error_detail.should include("Cannot charge a customer that has no active card") + purchase.billing_attempts.should eql 1 + booking.is_suspended?.should be_false + purchase.billed.should be false + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 2 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 3 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 4 + booking.reload + booking.is_suspended?.should be_false + purchase.billed.should be false + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + LessonBooking.bill_monthlies + LessonPackagePurchase.count.should eql 1 + purchase.reload + purchase.billing_attempts.should eql 5 + booking.reload + booking.is_suspended?.should be_true + purchase.billed.should be false + + # now that it's suspended, let's unsuspend it + user.card_approved(create_stripe_token, '78759') + user.save! + + day = day + 1 + time = day.to_time + Timecop.freeze(time) + purchase.bill_monthly(true) + LessonPackagePurchase.count.should eql 1 + purchase.reload + + purchase.month.should eql 1 + purchase.year.should eql 2016 + purchase.lesson_booking.should eql booking + purchase.teacher.should eql teacher_user + purchase.user.should eql user + purchase.billed.should eql true + purchase.billed_at.should eql time + # purchase.billing_error_reason.should be nil + purchase.billing_attempts.should eql 6 + purchase.post_processed.should eql true + purchase.post_processed_at.should eql time + purchase.last_billing_attempt_at.should eql time + purchase.sent_billing_notices.should eql true + booking.reload + booking.is_suspended?.should be_false + end + + it "missed meetings deduct on next month" do + # TODO. Discuss with David a little more + end + end + + describe "billable_monthlies" do + before do + Timecop.return + end + + it "empty" do + LessonBooking.billable_monthlies(Time.now).count.should eql 0 + end + + it "one" do + time = Date.new(2016, 1, 1) + Timecop.freeze(time) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + + now = Time.now + billables = LessonBooking.billable_monthlies(now) + billables.all.should eql [booking] + LessonPackagePurchase.where(lesson_booking_id: booking.id).count.should eql 0 + # to make this billable monthly go away, we will need to create one LessonPackagePurchase; one for this month (because it's only one we have lessons in) + # and further, mark them both as post_processed + + package = LessonPackagePurchase.create(user, booking, LessonPackageType.single, 2016, 1) + + LessonBooking.billable_monthlies(now).count.should eql 1 + + package.post_processed = true + package.save! + + LessonBooking.billable_monthlies(now).count.should eql 0 + end + end + + describe "predicted_times_for_month" do + after do + Timecop.return + end + it "fills up month" do + + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 5 + times[0].to_date.should eql (jan1) + times[1].to_date.should eql (Date.new(next_year, 1, 8)) + times[2].to_date.should eql (Date.new(next_year, 1, 15)) + times[3].to_date.should eql (Date.new(next_year, 1, 22)) + times[4].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "fills up partial month" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan15) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 3 + times[0].to_date.should eql (Date.new(next_year, 1, 15)) + times[1].to_date.should eql (Date.new(next_year, 1, 22)) + times[2].to_date.should eql (Date.new(next_year, 1, 29)) + end + + it "let's assume JamKazam is messed up for a few weeks" do + next_year = Time.now.year + 1 + jan1 = Date.new(next_year, 1, 1) + jan15 = Date.new(next_year, 1, 15) + jan31 = Date.new(next_year, 1, -1) + Timecop.freeze(jan1) + slot = valid_recurring_slots[0] + slot.day_of_week = jan1.wday + + # book a session on jan1 + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + # but don't run the computation of times per month for weeks + Timecop.freeze(Date.new(next_year, 1, 23)) + times = booking.predicted_times_for_month(next_year, 1) + times.length.should eql 2 + times[0].to_date.should eql (Date.new(next_year, 1, 1)) + times[1].to_date.should eql (Date.new(next_year, 1, 29)) + end + end + + describe "book_free" do + it "works" do + + pending "free not supported" + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_ELSEWHERE + booking.recurring.should eq false + booking.lesson_length.should eq 30 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_FREE + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 0 + user.remaining_test_drives.should eq 1 + + booking.card_presumed_ok.should eq false + booking.sent_notices.should eq false + + user.card_approved(create_stripe_token, '78759') + user.save! + booking.reload + booking.sent_notices.should eq true + booking.card_presumed_ok.should eq true + + end + + it "allows long message to flow through chat" do + + user.has_free_lessons?.should be_true + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without free lesson" do + + pending "free not supported" + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:user].should eq ["have no remaining free lessons"] + + ChatMessage.count.should eq 1 + end + + it "prevents user without stored credit card" do + + pending "free not supported" + user.stored_credit_card = false + user.save! + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + end + + it "must have 2 lesson booking slots" do + + booking = LessonBooking.book_test_drive(user, teacher_user, [], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["must have two times specified"] + end + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_free(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_test_drive" do + it "works" do + user.stored_credit_card = true + user.save! + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_ELSEWHERE + booking.recurring.should eq false + booking.lesson_length.should eq 30 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_TEST_DRIVE + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 0 + end + + it "allows long message to flow through chat" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, Faker::Lorem.characters(10000)) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "prevents user without remaining test drives" do + + user.stored_credit_card = true + user.save! + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + ChatMessage.count.should eq 1 + user.reload + user.remaining_test_drives.should eql 0 + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:user].should eq ["has a requested TestDrive with this teacher"] + + ChatMessage.count.should eq 1 + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_single, minute: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_single1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:minute].should eq ["is not a number"] + end + end + + describe "book_normal" do + it "works" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_SINGLE + booking.recurring.should eq false + booking.lesson_length.should eq 60 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + it "works with recurring slots" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + booking.user.should eq user + booking.teacher.should eq teacher_user + booking.description.should eq ("Hey I've heard of you before.") + booking.payment_style.should eq LessonBooking::PAYMENT_STYLE_WEEKLY + booking.recurring.should eq true + booking.lesson_length.should eq 60 + booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID + booking.lesson_booking_slots.length.should eq 2 + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + + user.reload + user.remaining_free_lessons.should eq 1 + user.remaining_test_drives.should eq 1 + end + + it "allows long message to flow through chat" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, Faker::Lorem.characters(10000), true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + booking.errors.any?.should be false + + chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message.should_not be_nil + chat_message.message.should eq booking.description + end + + it "does not prevent user without remaining test drives" do + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevents user without free lesson" do + + booking = LessonBooking.book_free(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be false + + ChatMessage.count.should eq 1 + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + + ChatMessage.count.should eq 2 + end + + it "does not prevent user without a stored credit card" do + user.stored_credit_card = false + user.save! + + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.errors.any?.should be false + booking.card_presumed_ok.should eq false + booking.sent_notices.should eq false + end + + + it "must have well-formed booking slots" do + bad_slot = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: nil) + booking = LessonBooking.book_test_drive(user, teacher_user, [lesson_booking_slot_recurring1, bad_slot], "Hey I've heard of you before.") + booking.errors.any?.should be true + booking.errors[:lesson_booking_slots].should eq ["is invalid"] + bad_slot = booking.lesson_booking_slots[1] + bad_slot.errors[:day_of_week].should eq ["must be specified"] + end + end + + describe "find_bookings_needing_sessions" do + after do + Timecop.return + end + it "can detect missing lesson and schedules it 1 week out" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], teacher_user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 2 + + lesson_session = booking.lesson_sessions[0] + Timecop.freeze(lesson_session.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 3 + + # check that all the times make sense + + lesson1 = booking.lesson_sessions[0] + lesson2 = booking.lesson_sessions[1] + lesson3 = booking.lesson_sessions[2] + lesson1.music_session.scheduled_start.to_i.should eql (lesson2.music_session.scheduled_start.to_i - (60 * 60 * 24 * 7)) + lesson1.music_session.scheduled_start.to_i.should eql (lesson3.music_session.scheduled_start.to_i - (60 * 60 * 24 * 14)) + + Timecop.freeze(lesson2.music_session.scheduled_start) + + # causes find_bookings_needing_sessions to re-run + booking.sync_lessons + booking.reload + booking.lesson_sessions.length.should eql 4 + + # check that all the times make sense + + lesson4 = booking.lesson_sessions[3] + lesson1.music_session.scheduled_start.to_i.should eql (lesson4.music_session.scheduled_start.to_i - (60 * 60 * 24 * 21)) + end + + it "ignores non-recurring" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + booking.lesson_sessions.length.should eql 1 + + booking.accept(booking.lesson_sessions[0], booking.lesson_booking_slots[0], user) + booking.errors.any?.should be false + booking.reload + booking.lesson_sessions.length.should eql 1 + booking.accepter.should eql user + end + end + + describe "canceling" do + + after do + Timecop.return + end + + it "single session gets canceled before accepted" do + + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # avoid 24 hour problem + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: teacher_user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 1 + end + + it "single session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + UserMailer.deliveries.length.should eql 2 + end + + it "recurring session gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + booking.active.should eql false + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.active.should eql true + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + lesson_session.canceler.should eql user + booking.reload + booking.active.should eql true + booking.status.should eql LessonSession::STATUS_APPROVED + booking.canceler.should be_nil + UserMailer.deliveries.length.should eql 2 + end + + it "recurring booking gets canceled after accepted" do + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: booking.default_slot.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + UserMailer.deliveries.clear + Timecop.freeze(7.days.ago) + lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: true}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_CANCELED + lesson_session.reload + booking.reload + booking.status.should eql LessonSession::STATUS_CANCELED + booking.canceler.should eql user + UserMailer.deliveries.length.should eql 2 + end + end + describe "rescheduling" do + + after do + Timecop.return + end + it "non-recurring, accepted with new slot" do + booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_single, hour: 16) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + booking.reload + booking.counter_slot.should eql counter + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + booking.reload + booking.accepter.should eql teacher_user + booking.counter_slot.should be_nil + end + + it "recurring" do + Timecop.freeze(Time.new(2016, 03, 4, 5, 0, 0)) + booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) + lesson_session = booking.lesson_sessions[0] + lesson_session.status.should eql LessonSession::STATUS_REQUESTED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + counter = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 2) + lesson_session.counter({proposer: user, slot: counter, message: 'Does this work better?'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) + + # to help scoot out the 'created_at' of the lessons + Timecop.freeze(Time.now + 10) + + lesson_session.accept({accepter: teacher_user, message: 'Yeah I got this', slot: counter.id, update_all: false}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.status.should eql LessonSession::STATUS_APPROVED + + lesson_session.reload + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session2 = booking.lesson_sessions.order(:created_at)[1] + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + + # now it's approved, we have 2 sessions that are not yet completed with a time + + # we should be able to reschedule just one of the lessons + counter2 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 4, update_all: false) + lesson_session.counter({proposer: user, slot: counter2, message: 'ACtually, let\'s do this instead for just this one'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.scheduled_start.should eql counter.scheduled_time(0) + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix just this one', slot: counter2.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) # STILL ORIGINAL COUNTER! + + # we should be able to reschedule all of the lessons + + + counter3 = FactoryGirl.build(:lesson_booking_slot_recurring, day_of_week: 5, update_all: true) + lesson_session.counter({proposer: user, slot: counter3, message: 'ACtually, let\'s do this instead for just this one... again'}) + lesson_session.errors.any?.should be false + lesson_session.status.should eql LessonSession::STATUS_COUNTERED + lesson_session.reload + lesson_session2.reload + lesson_session.scheduled_start.should eql counter2.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter.scheduled_time(1) + booking.reload + booking.counter_slot.should eql counter3 + + + lesson_session.accept({accepter: teacher_user, message: 'OK, lets fix all of them', slot: counter3.id}) + lesson_session.errors.any?.should be_false + lesson_session.status.should eql LessonSession::STATUS_APPROVED + booking.reload + booking.counter_slot.should be_nil + lesson_session.reload + lesson_session2.reload + lesson_session.created_at.should be < lesson_session2.created_at + + lesson_session.scheduled_start.should eql counter3.scheduled_time(0) + lesson_session2.scheduled_start.should eql counter3.scheduled_time(1 ) + + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb new file mode 100644 index 000000000..5d542f0c4 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe LessonPackagePurchase do + + let(:user) {FactoryGirl.create(:user)} + let(:lesson_booking) {FactoryGirl.create(:lesson_booking)} + +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb new file mode 100644 index 000000000..fcd2816b5 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_analyser_spec.rb @@ -0,0 +1,257 @@ +require 'spec_helper' + +describe LessonSessionAnalyser do + + let(:user) { FactoryGirl.create(:user, remaining_test_drives: 1) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "analyse" do + it "teacher joined on time, waited, student no show" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis[:student].should eql LessonSessionAnalyser::NO_SHOW + analysis[:bill].should be true + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be false + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql 0 + end + + it "teacher joined on time, waited, student joined late" do + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + late_start = start + (7 * 60) + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: late_start, session_removed_at: late_start + 4 * 60) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + + analysis = LessonSessionAnalyser.analyse(lesson) + puts analysis + analysis[:reason].should eql LessonSessionAnalyser::STUDENT_FAULT + analysis[:student].should eql LessonSessionAnalyser::JOINED_LATE + analysis[:bill].should be true + + student = analysis[:student_analysis] + student[:joined_on_time].should be false + student[:joined_late].should be true + student[:waited_correctly].should be false + student[:initial_waiting_pct].should eql nil + student[:potential_waiting_time].should eql nil + + together = analysis[:together_analysis] + together[:session_time].should eql (4 * 60.to_f) + end + + it "together 5 minutes" do + + lesson = testdrive_lesson(user, teacher) + music_session = lesson.music_session + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + + # create some bogus, super-perfect teacher/student times + start = lesson.scheduled_start + end_time = lesson.scheduled_start + (60 * lesson.duration) + uh1 = FactoryGirl.create(:music_session_user_history, user: user, history: music_session, created_at: start, session_removed_at: end_time) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher, history: music_session, created_at: start, session_removed_at: end_time) + + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SESSION_ONGOING + + lesson.music_session.session_removed_at = end_time + lesson.music_session.save! + + analysis = LessonSessionAnalyser.analyse(lesson) + analysis[:reason].should eql LessonSessionAnalyser::SUCCESS + analysis[:bill].should be true + + teacher = analysis[:teacher_analysis] + teacher[:joined_on_time].should be true + teacher[:waited_correctly].should be true + teacher[:initial_waiting_pct].should eql 1.0 + teacher[:potential_waiting_time].should eql 600.0 + + together = analysis[:together_analysis] + together[:session_time].should eql 1800.0 + end + end + + describe "intersecting_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + it "empty" do + LessonSessionAnalyser.intersecting_ranges([], []).should eql [] + end + + it "one specified, other empty" do + + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([], [Range.new(uh1Begin, uh1End)]).should eql [] + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], []).should eql [] + end + + it "both identical" do + uh1Begin = start + uh1End = start + 1 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh1End)] + end + + it "one intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 1 + uh2End = start + 3 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [Range.new(uh2Begin, uh2End)] + end + + it "overlapping intersect" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + -1 + uh2End = start + 2 + + uh3Begin = start + 3 + uh3End = start + 5 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh2Begin, uh2End), Range.new(uh3Begin, uh3End)], [Range.new(uh1Begin, uh1End)]).should eql [Range.new(uh1Begin, uh2End), Range.new(uh3Begin, uh1End)] + end + + it "no overlap" do + uh1Begin = start + uh1End = start + 4 + + uh2Begin = start + 5 + uh2End = start + 6 + + LessonSessionAnalyser.intersecting_ranges([Range.new(uh1Begin, uh1End)], [Range.new(uh2Begin, uh2End)]).should eql [] + end + end + + describe "merge_overlapping_ranges" do + + let(:lesson) { testdrive_lesson(user, teacher) } + let(:music_session) { lesson.music_session } + + + it "empty" do + LessonSessionAnalyser.merge_overlapping_ranges([]).should eql [] + end + + it "one item" do + uh1Begin = start + uh1End = start + 1 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + + ranges = LessonSessionAnalyser.time_ranges [uh1] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two identical items" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = uh1Begin + uh2End = uh1End + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End)] + end + + it "two separate times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 3 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh1End), Range.new(uh2Begin, uh2End)] + end + + it "two overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh2End)] + end + + it "three overlapping times" do + uh1Begin = start + uh1End = start + 1 + + uh2Begin = start + 0.5 + uh2End = start + 5 + + uh3Begin = start + 0.1 + uh3End = start + 6 + + uh1 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh1Begin, session_removed_at: uh1End) + uh2 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh2Begin, session_removed_at: uh2End) + uh3 = FactoryGirl.build(:music_session_user_history, user: user, history: music_session, created_at: uh3Begin, session_removed_at: uh3End) + + ranges = LessonSessionAnalyser.time_ranges [uh1, uh2, uh3] + + LessonSessionAnalyser.merge_overlapping_ranges(ranges).should eql [Range.new(uh1Begin, uh3End)] + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb new file mode 100644 index 000000000..0f254d5e7 --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_monthly_price_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe LessonSessionMonthlyPrice do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + let(:start) { Time.now } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + + describe "price" do + after do + Timecop.return + end + + it "start of the month" do + + jan1 = Date.new(2016, 1, 1) + Timecop.freeze(jan1) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + booking.booked_price.should eql(LessonSessionMonthlyPrice.price(booking, jan1)) + end + + it "middle of the month" do + + jan17 = Date.new(2016, 1, 17) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.75).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which has a last billable day based on slot" do + + jan17 = Date.new(2016, 1, 31) + Timecop.freeze(jan17) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.25).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, jan17)) + end + + it "end of the month which is not a last billable days" do + + feb29 = Date.new(2016, 2, -1) + Timecop.freeze(feb29) + + booking = LessonBooking.book_normal(user, teacher, valid_recurring_slots, "Monthly time!", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60) + + booking.booked_price.should eql 30.00 + + ((booking.booked_price * 0.0).round(2)).should eql(LessonSessionMonthlyPrice.price(booking, feb29)) + end + end +end diff --git a/ruby/spec/jam_ruby/models/lesson_session_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_spec.rb new file mode 100644 index 000000000..e1470bdff --- /dev/null +++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe LessonSession do + + let(:user) {FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1)} + let(:teacher) {FactoryGirl.create(:teacher_user)} + let(:slot1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:slot2) { FactoryGirl.build(:lesson_booking_slot_single) } + + let(:lesson_booking) {LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)} + let(:lesson_session) {lesson_booking.lesson_sessions[0]} + + describe "accept" do + it "can accept" do + + end + end + + describe "index" do + it "finds single lesson as student" do + + lesson_booking.touch + lesson_session.touch + lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user + lesson_session.lesson_booking.teacher.should eql teacher + + query = LessonSession.index(user)[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user))[:query] + query.length.should eq 0 + end + + it "finds single lesson as teacher" do + + # just sanity check that the lesson_session Factory is doing what it should + lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user + lesson_session.lesson_booking.teacher.should eql teacher + + query = LessonSession.index(teacher, {as_teacher: true})[:query] + query.length.should eq 1 + + # make sure some random nobody can see this lesson session + query = LessonSession.index(FactoryGirl.create(:user), {as_teacher: true})[:query] + query.length.should eq 0 + end + end +end diff --git a/ruby/spec/jam_ruby/models/review_spec.rb b/ruby/spec/jam_ruby/models/review_spec.rb index 7a5851948..3deed6f3b 100644 --- a/ruby/spec/jam_ruby/models/review_spec.rb +++ b/ruby/spec/jam_ruby/models/review_spec.rb @@ -16,7 +16,7 @@ describe Review do context "validates review" do it "blank target" do - review = Review.create() + review = Review.create({}) review.valid?.should be_false review.errors[:target].should == ["can't be blank"] end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index fc89145cb..bc278b7e2 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' describe Sale do - let(:user) {FactoryGirl.create(:user)} - let(:user2) {FactoryGirl.create(:user)} - let(:jam_track) {FactoryGirl.create(:jam_track)} - let(:jam_track2) {FactoryGirl.create(:jam_track)} - let(:jam_track3) {FactoryGirl.create(:jam_track)} - let(:gift_card) {GiftCardType.jam_track_5} + let(:user) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:jam_track2) { FactoryGirl.create(:jam_track) } + let(:jam_track3) { FactoryGirl.create(:jam_track) } + let(:gift_card) { GiftCardType.jam_track_5 } def assert_free_line_item(sale_line_item, jamtrack) sale_line_item.recurly_tax_in_cents.should be_nil @@ -63,7 +63,7 @@ describe Sale do describe "place_order" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } let(:jamtrack) { FactoryGirl.create(:jam_track) } let(:jamtrack2) { FactoryGirl.create(:jam_track) } let(:jamtrack3) { FactoryGirl.create(:jam_track) } @@ -265,7 +265,6 @@ describe Sale do user.gifted_jamtracks.should eq(1) - # OK! Now make a second purchase; this time, buy one free, one not free shopping_cart3 = ShoppingCart.create user, jamtrack3, 1, true @@ -567,6 +566,291 @@ describe Sale do end end + describe "lessons" do + + + let(:teacher_user) { FactoryGirl.create(:teacher_user) } + let(:teacher) { teacher_user.teacher } + let(:lesson_booking_slot_single1) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_single2) { FactoryGirl.build(:lesson_booking_slot_single) } + let(:lesson_booking_slot_recurring1) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:lesson_booking_slot_recurring2) { FactoryGirl.build(:lesson_booking_slot_recurring) } + let(:valid_single_slots) { [lesson_booking_slot_single1, lesson_booking_slot_single2] } + let(:valid_recurring_slots) { [lesson_booking_slot_recurring1, lesson_booking_slot_recurring2] } + let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } + + describe "single" do + it "can succeed" do + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 3000 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 + end + + it "affiliate" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', normal: true}) + + lesson_session = normal_lesson(user, teacher_user) + + # teacher & student get into session + start = lesson_session.scheduled_start + end_time = lesson_session.scheduled_start + (60 * lesson_session.duration) + uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time) + # artificially end the session, which is covered by other background jobs + lesson_session.music_session.session_removed_at = end_time + lesson_session.music_session.save! + + # bill the user + LessonSession.hourly_check + + lesson_session.reload + payment = lesson_session.lesson_payment_charge + puts lesson_session.billing_error_reason + puts lesson_session.billing_error_detail + user.reload + user.sales.count.should eql 1 + sale = user.sales.first + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 248 + sale.recurly_total_in_cents.should eql 3248 + sale.recurly_subtotal_in_cents.should eql 3000 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.single.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 0 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 30.00 + lesson_purchase.lesson_package_type.is_normal?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 2 + affiliate_partner.reload + affiliate_partner2.reload + affiliate_partner.affiliate_distributions.count.should eql 1 + affiliate_partner2.affiliate_distributions.count.should eql 1 + partner1_distribution = affiliate_partner.affiliate_distributions.first + partner2_distribution = affiliate_partner2.affiliate_distributions.first + partner1_distribution.sale_line_item.should eql partner2_distribution.sale_line_item + partner1_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner.lesson_rate).round + partner2_distribution.affiliate_referral_fee_in_cents.should eql (3000 * affiliate_partner2.lesson_rate).round + end + + it "book recurring, monthly" do + + end + + end + describe "purchase_test_drive" do + it "can succeed" do + user.affiliate_referral = affiliate_partner + user.save! + teacher_user.affiliate_referral = affiliate_partner2 + teacher_user.save! + + + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + + token = create_stripe_token + result = user.payment_update({token: token, zip: '72205', test_drive: true}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be 0 + sale.recurly_total_in_cents.should eql 4999 + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive.id + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + sale.sale_line_items.count.should eql 1 + line_item = sale.sale_line_items[0] + line_item.reload + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with tax" do + #user.remaining_test_drives = 0 + #user.save! + booking = LessonBooking.book_test_drive(user, teacher_user, valid_single_slots, "Hey I've heard of you before.") + booking.errors.any?.should be_false + booking.user.should eql user + booking.card_presumed_ok.should be_false + booking.should eql user.unprocessed_test_drive + user.reload + user.remaining_test_drives.should eql 0 + + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + + booking.reload + booking.card_presumed_ok.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive.id + + + user.reload + user.stripe_customer_id.should_not be nil + user.lesson_purchases.length.should eql 1 + lesson_purchase = booking.lesson_sessions[0].lesson_package_purchase + user.remaining_test_drives.should eql 3 + lesson_purchase = user.lesson_purchases[0] + lesson_purchase.price.should eql 49.99 + lesson_purchase.lesson_package_type.is_test_drive?.should eql true + + customer = Stripe::Customer.retrieve(user.stripe_customer_id) + customer.email.should eql user.email + + booking.lesson_sessions[0].accept({message: "got it", slot: booking.lesson_booking_slots[0].id}) + user.reload + user.remaining_test_drives.should eql 3 + line_item.affiliate_distributions.count.should eql 0 # test drives don't create affiliate + end + + it "can succeed with no booking; just intent" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + + user.sales.count.should eql 1 + sale = result[:test_drive] + + sale.stripe_charge_id.should_not be_nil + sale.recurly_tax_in_cents.should be (4999 * 0.0825).round + sale.recurly_total_in_cents.should eql 4999 + (4999 * 0.0825).round + sale.recurly_subtotal_in_cents.should eql 4999 + sale.recurly_currency.should eql 'USD' + line_item = sale.sale_line_items[0] + line_item.quantity.should eql 1 + line_item.product_type.should eql SaleLineItem::LESSON + line_item.product_id.should eq LessonPackageType.test_drive.id + end + + it "will reject second test drive purchase" do + intent = TeacherIntent.create(user, teacher, 'book-test-drive') + token = create_stripe_token + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_false + + result = user.payment_update({token: token, zip: '78759', test_drive: true}) + user.errors.any?.should be_false + user.reload + user.has_stored_credit_card?.should be_true + user.sales.count.should eql 1 + purchase = result[:purchase] + purchase.errors.any?.should be_true + purchase.errors[:user].should eq ["can not buy test drive right now because you have already purchased it within the last year"] + SaleLineItem.count.should eql 1 + Sale.count.should eql 1 + + + end + end + end + describe "check_integrity_of_jam_track_sales" do let(:user) { FactoryGirl.create(:user) } diff --git a/ruby/spec/jam_ruby/models/school_invitation_spec.rb b/ruby/spec/jam_ruby/models/school_invitation_spec.rb new file mode 100644 index 000000000..b851d434f --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_invitation_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe SchoolInvitation do + + let(:school) {FactoryGirl.create(:school)} + + it "created by factory" do + FactoryGirl.create(:school_invitation) + end + + it "created by method" do + SchoolInvitation.create(school.user, school, {as_teacher: true, first_name: "Bobby", last_name: "Jimes", email: "somewhere@jamkazam.com"}) + end + + describe "index" do + it "works" do + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 0 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: true) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 0 + + FactoryGirl.create(:school_invitation, school: school, as_teacher: false) + SchoolInvitation.index(school, {as_teacher: true})[:query].count.should eql 1 + SchoolInvitation.index(school, {as_teacher: false})[:query].count.should eql 1 + + end + + + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/school_spec.rb b/ruby/spec/jam_ruby/models/school_spec.rb new file mode 100644 index 000000000..0eac36175 --- /dev/null +++ b/ruby/spec/jam_ruby/models/school_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe School do + + it "created by factory" do + FactoryGirl.create(:school) + end + + it "has correct associations" do + school = FactoryGirl.create(:school) + + school.should eql school.user.owned_school + + student = FactoryGirl.create(:user, school: school) + teacher = FactoryGirl.create(:teacher, school: school) + + school.reload + school.students.should eql [student] + school.teachers.should eql [teacher] + + student.school.should eql school + teacher.school.should eql school + end + + it "updates" do + school = FactoryGirl.create(:school) + school.update_from_params({name: 'hahah', scheduling_communication: 'school', correspondence_email: 'bobby@jamkazam.com'}) + school.errors.any?.should be_false + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb new file mode 100644 index 000000000..58ac4d16f --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb @@ -0,0 +1,293 @@ +require 'spec_helper' + +describe TeacherPayment do + + let(:user) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:teacher_obj) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account1_id)} + let(:teacher_obj2) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account2_id)} + let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) } + let(:teacher2) { FactoryGirl.create(:user, teacher: teacher_obj2) } + let(:test_drive_lesson) {testdrive_lesson(user, teacher)} + let(:test_drive_lesson2) {testdrive_lesson(user2, teacher2)} + let(:test_drive_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson, teacher: teacher, teacher_payment: nil, ready:false)} + let(:test_drive_distribution2) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson2, teacher: teacher2, teacher_payment: nil, ready:false)} + let(:normal_lesson_session) {normal_lesson(user, teacher)} + let(:normal_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: normal_lesson_session, teacher: teacher, teacher_payment: nil, ready:false)} + + describe "pending_teacher_payments" do + + it "empty" do + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "one distribution" do + test_drive_distribution.touch + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 1 + payments[0]['id'].should eql teacher.id + end + + it "multiple teachers" do + test_drive_distribution.touch + test_drive_distribution2.touch + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 0 + + test_drive_distribution.ready = true + test_drive_distribution.save! + test_drive_distribution2.ready = true + test_drive_distribution2.save! + + payments = TeacherPayment.pending_teacher_payments + payments.count.should eql 2 + payment_user_ids = payments.map(&:id) + payment_user_ids.include? teacher.id + payment_user_ids.include? teacher2.id + end + end + + describe "teacher_payments" do + it "empty" do + TeacherPayment.teacher_payments + end + + it "charges test drive" do + test_drive_distribution.touch + + test_drive_distribution.ready = true + test_drive_distribution.save! + + TeacherPayment.teacher_payments + + test_drive_distribution.reload + test_drive_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = test_drive_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + charge.application_fee.should eql nil + + TeacherPayment.pending_teacher_payments.count.should eql 0 + end + + it "charges normal" do + normal_distribution.touch + + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + charge.application_fee.should include("fee_") + end + + it "charges multiple" do + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 2 + + payment = normal_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + charge.application_fee.should include("fee_") + + test_drive_distribution.reload + payment = test_drive_distribution.teacher_payment + + if payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_reason + puts payment.teacher_payment_charge.billing_error_detail + end + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + charge.application_fee.should be_nil + end + + + + describe "stripe mocked" do + before { StripeMock.start } + after { StripeMock.stop; Timecop.return } + + it "failed payment, then success" do + StripeMock.prepare_card_error(:card_declined) + + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billing_error_reason.should eql("card_declined") + payment.teacher_payment_charge.billing_error_detail.should include("declined") + + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + payment.teacher_payment_charge.stripe_charge_id.should be_nil + + StripeMock.clear_errors + + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.teacher_payment_charge.billed.should eql false + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + TeacherPayment.teacher_payments + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 1 + + # make sure the teacher_payment is reused, and charge is reused + normal_distribution.teacher_payment.should eql(payment) + normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge) + + # no attempt should be made because a day hasn't gone by + payment = normal_distribution.teacher_payment + payment.reload + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + end + + it "charges multiple (with initial failure)" do + StripeMock.prepare_card_error(:card_declined) + + test_drive_distribution.touch + test_drive_distribution.ready = true + test_drive_distribution.save! + normal_distribution.touch + normal_distribution.ready = true + normal_distribution.save! + + TeacherPayment.teacher_payments + + TeacherPayment.count.should eql 1 + payment = TeacherPayment.first + payment.teacher_payment_charge.billed.should be_false + + # advance one day so that a charge is attempted again + Timecop.freeze(Date.today + 2) + + StripeMock.clear_errors + TeacherPayment.teacher_payments + + normal_distribution.reload + normal_distribution.teacher_payment.should_not be_nil + TeacherPayment.count.should eql 2 + + payment = normal_distribution.teacher_payment + + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 280 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + + test_drive_distribution.reload + payment = test_drive_distribution.teacher_payment + + payment.teacher_payment_charge.billed.should eql true + payment.teacher_payment_charge.amount_in_cents.should eql 1000 + payment.teacher_payment_charge.fee_in_cents.should eql 0 + teacher_distribution = payment.teacher_payment_charge.distribution + teacher_distribution.amount_in_cents.should eql 1000 + charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) + charge.amount.should eql 1000 + end + end + + end +end diff --git a/ruby/spec/jam_ruby/models/teacher_spec.rb b/ruby/spec/jam_ruby/models/teacher_spec.rb index 7ec2cf2e5..3b0c305d0 100644 --- a/ruby/spec/jam_ruby/models/teacher_spec.rb +++ b/ruby/spec/jam_ruby/models/teacher_spec.rb @@ -14,14 +14,14 @@ describe Teacher do describe "index" do it "no params" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil)[:query] teachers.length.should eq 1 teachers[0].should eq(teacher.user) end it "instruments" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {instruments: ['acoustic guitar']})[:query] teachers.length.should eq 0 @@ -39,7 +39,7 @@ describe Teacher do end it "subjects" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {subjects: ['music-theory']})[:query] teachers.length.should eq 0 @@ -51,7 +51,7 @@ describe Teacher do end it "genres" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {genres: ['ambient']})[:query] teachers.length.should eq 0 @@ -64,7 +64,7 @@ describe Teacher do it "languages" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {languages: ['EN']})[:query] teachers.length.should eq 0 @@ -76,7 +76,7 @@ describe Teacher do end it "country" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {country: 'DO'})[:query] teachers.length.should eq 0 @@ -88,7 +88,7 @@ describe Teacher do end it "region" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {region: 'HE'})[:query] teachers.length.should eq 0 @@ -100,7 +100,7 @@ describe Teacher do end it "years_teaching" do - teacher = FactoryGirl.create(:teacher, years_teaching: 5) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now, years_teaching: 5) teachers = Teacher.index(nil, {years_teaching: 10})[:query] teachers.length.should eq 0 @@ -110,7 +110,7 @@ describe Teacher do end it "teaches beginner/intermediate/advanced" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {teaches_beginner: true})[:query] teachers.length.should eq 0 @@ -133,7 +133,7 @@ describe Teacher do end it "student_age" do - teacher = FactoryGirl.create(:teacher) + teacher = FactoryGirl.create(:teacher, ready_for_session_at: Time.now) teachers = Teacher.index(nil, {student_age: 5})[:query] teachers.length.should eq 1 @@ -213,6 +213,7 @@ describe Teacher do years_playing: 12 ) teacher.should_not be_nil + teacher.user.should eql user teacher.errors.should be_empty teacher.id.should_not be_nil t = Teacher.find(teacher.id) @@ -347,6 +348,11 @@ describe Teacher do end describe "validates" do + it "barebones" do + teacher = Teacher.save_teacher(user, validate_introduction: true, biography: "Teaches for dat school") + teacher.errors.should be_empty + + end it "introduction" do teacher = Teacher.save_teacher( user, diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 89abe6e16..36e3cf949 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -97,7 +97,7 @@ describe User do it { should be_admin } end - + describe "when first name is not present" do before { @user.first_name = " " } @@ -136,14 +136,13 @@ describe User do end - describe "first or last name cant have profanity" do it "should not let the first name have profanity" do @user.first_name = "fuck you" @user.save @user.should_not be_valid end - + it "should not let the last name have profanity" do @user.last_name = "fuck you" @user.save @@ -226,7 +225,7 @@ describe User do User.authenticate(@user.email, "newpassword").should_not be_nil UserMailer.deliveries.length.should == 1 end - + it "setting a new password should fail if old one doesnt match" do @user.set_password("wrongold", "newpassword", "newpassword") @user.errors.any?.should be_true @@ -240,7 +239,7 @@ describe User do @user.errors[:password].length.should == 1 UserMailer.deliveries.length.should == 0 end - + it "setting a new password should fail if new one doesnt validate" do @user.set_password("foobar", "a", "a") @user.errors.any?.should be_true @@ -254,15 +253,15 @@ describe User do @user.errors[:password].length.should == 1 UserMailer.deliveries.length.should == 0 end - + end - + describe "reset_password" do before do @user.confirm_email! @user.save end - + it "fails if the provided email address is unrecognized" do expect { User.reset_password("invalidemail@invalid.com", RESET_PASSWORD_URL) }.to raise_error(JamRuby::JamArgumentError) end @@ -331,7 +330,7 @@ describe User do describe "authenticate (class-instance)" do before { @user.email_confirmed=true; @user.save } - + describe "with valid password" do it { should == User.authenticate(@user.email, @user.password) } end @@ -354,7 +353,7 @@ describe User do end describe "create_dev_user" do - before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } + before { @dev_user = User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } subject { @dev_user } @@ -365,7 +364,7 @@ describe User do describe "should not be a new record" do it { should be_persisted } end - + describe "updates record" do before { @dev_user = User.create_dev_user("Seth", "Call2", "seth@jamkazam.com", "Jam123", "Austin", "Texas", "US", nil, nil) } @@ -470,22 +469,22 @@ describe User do describe "finalize email updates recurly" do before do - + @user.begin_update_email("somenewemail@blah.com", "foobar", "http://www.jamkazam.com/confirm_email_update?token=") UserMailer.deliveries.clear billing_info = { - first_name: @user.first_name, - last_name: @user.last_name, - address1: 'Test Address 1', - address2: 'Test Address 2', - city: @user.city, - state: @user.state, - country: @user.country, - zip: '12345', - number: '4111-1111-1111-1111', - month: '08', - year: '2017', - verification_value: '111' + first_name: @user.first_name, + last_name: @user.last_name, + address1: 'Test Address 1', + address2: 'Test Address 2', + city: @user.city, + state: @user.state, + country: @user.country, + zip: '12345', + number: '4111-1111-1111-1111', + month: '08', + year: '2017', + verification_value: '111' } @recurly.find_or_create_account(@user, billing_info) end @@ -495,30 +494,30 @@ describe User do @recurly.get_account(@user).email.should_not == "somenewemail@blah.com" @finalized = User.finalize_update_email(@user.update_email_token) @recurly.get_account(@user).email.should == "somenewemail@blah.com" - end + end end describe "user_authorizations" do it "can create" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! end it "fails on duplicate" do - @user.user_authorizations.build provider: 'facebook', - uid: '1', - token: '1', - token_expiration: Time.now, - user: @user + @user.user_authorizations.build provider: 'facebook', + uid: '1', + token: '1', + token_expiration: Time.now, + user: @user @user.save! @user2 = FactoryGirl.create(:user) - @user2.user_authorizations.build provider: 'facebook', + @user2.user_authorizations.build provider: 'facebook', uid: '1', token: '1', token_expiration: Time.now, @@ -532,7 +531,7 @@ describe User do describe "mods" do it "should allow update of JSON" do - @user.mods = {no_show: {something:1}}.to_json + @user.mods = {no_show: {something: 1}}.to_json @user.save! end end @@ -623,7 +622,7 @@ describe User do end it "remains null if the user's last_jam_addr is null" do - @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start + @user.last_jam_addr.should be_nil # make sure the factory still makes a null addr to start User.update_locidispids(false) @user.reload @user.last_jam_addr.should be_nil @@ -700,7 +699,7 @@ describe User do end describe "age" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "should calculate age based on birth_date" do user.birth_date = Time.now - 10.years @@ -715,7 +714,7 @@ describe User do end describe "mods_merge" do - let(:user) {FactoryGirl.create(:user)} + let(:user) { FactoryGirl.create(:user) } it "allow empty merge" do user.mod_merge({}) @@ -726,7 +725,7 @@ describe User do it "allow no_show set" do user.mod_merge({"no_show" => {"some_screen" => true}}) user.valid?.should be_true - user.mods.should == {no_show:{some_screen:true}}.to_json + user.mods.should == {no_show: {some_screen: true}}.to_json end it "allow no_show aggregation" do @@ -744,22 +743,105 @@ describe User do user.reload user.mod_merge({"no_show" => {"some_screen1" => false}}) user.valid?.should be_true - user.mods.should == {no_show:{some_screen1:false}}.to_json + user.mods.should == {no_show: {some_screen1: false}}.to_json end it "does not allow random root keys" do - user.mod_merge({random_root_key:true}) + user.mod_merge({random_root_key: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_UNKNOWN_KEY] end it "does not allow non-hash no_show" do - user.mod_merge({no_show:true}) + user.mod_merge({no_show: true}) user.valid?.should be_false user.errors[:mods].should == [ValidationMessages::MODS_MUST_BE_HASH] end end + describe "sync_stripe_customer" do + let(:user) { FactoryGirl.create(:user) } + let(:token1) { create_stripe_token } + let(:token2) { create_stripe_token(2018) } + + # possible Stripe::InvalidRequestError + it "reuses user on card update" do + user.stripe_customer_id.should be_nil + user.payment_update({stripe_token: token1}) + user.reload + user.stripe_customer_id.should_not be_nil + customer1 = user.stripe_customer_id + + # let's change email address too + user.email = 'unique+1@jamkazam.com' + user.save! + + token2.should_not eql token1 + user.payment_update({stripe_token: token2}) + user.reload + user.stripe_customer_id.should_not be_nil + customer2 = user.stripe_customer_id + + customer1.should eql customer2 + # double-check that the stripe customer db record got it's email synced + customer = user.fetch_stripe_customer + customer.email.should eql 'unique+1@jamkazam.com' + + end + end + + describe "can_buy_test_drive?" do + let(:user) { FactoryGirl.create(:user) } + after { + Timecop.return + } + it "works" do + user.can_buy_test_drive?.should be true + FactoryGirl.create(:test_drive_purchase, user: user) + user.can_buy_test_drive?.should be false + Timecop.freeze(Date.today + 366) + user.can_buy_test_drive?.should be true + end + end + + describe "has_rated_teacher" do + + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher) } + it "works" do + user.has_rated_teacher(teacher).should eql false + review = Review.create(target:teacher, rating:3, user: user) + review.errors.any?.should be false + user.has_rated_teacher(teacher).should be true + end + end + + describe "recent_test_drive_teachers" do + let(:user) { FactoryGirl.create(:user) } + let(:teacher) { FactoryGirl.create(:teacher_user) } + it "works" do + user.recent_test_drive_teachers.count.should eql 0 + + testdrive_lesson(user, teacher) + + user.recent_test_drive_teachers[0].id.should eql teacher.id + + end + end + + describe "update_name" do + + let(:user) {FactoryGirl.create(:user)} + + it "typical 2 bits" do + user.update_name("Seth Call") + user.errors.any?.should be_false + user.reload + user.first_name.should eql 'Seth' + user.last_name.should eql 'Call' + end + end + =begin describe "update avatar" do diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index dfc84cef3..464f80697 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -34,10 +34,111 @@ describe "RenderMailers", :slow => true do describe "has sending user" do let(:user2) { FactoryGirl.create(:user) } - let(:friend_request) {FactoryGirl.create(:friend_request, user:user, friend: user2)} + let(:friend_request) { FactoryGirl.create(:friend_request, user: user, friend: user2) } it { @filename="text_message"; UserMailer.text_message(user, user2.id, user2.name, user2.resolved_photo_url, 'Get online!!').deliver } - it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver} + it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver } + end + + describe "student/teacher" do + let(:teacher) { u = FactoryGirl.create(:teacher); u.user } + let(:user) { FactoryGirl.create(:user) } + + it "teacher_lesson_request" do + @filename = "teacher_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_request(lesson_booking).deliver + end + + it "student_lesson_request" do + @filename = "student_lesson_request" + + lesson_booking = testdrive_lesson(user, teacher).lesson_booking + UserMailer.deliveries.clear + UserMailer.student_lesson_request(lesson_booking).deliver + end + + it "teacher_lesson_accepted" do + @filename = "teacher_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver + end + + it "student_lesson_accepted" do + @filename = "student_lesson_accepted" + + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_lesson_accepted(lesson_session, "custom message", lesson_session.lesson_booking.default_slot).deliver + end + + it "teacher_scheduled_jamclass_invitation" do + @filename = "teacher_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.teacher_scheduled_jamclass_invitation(lesson_session.teacher, "custom message", lesson_session.music_session).deliver + end + + it "student_scheduled_jamclass_invitation" do + @filename = "student_scheduled_jamclass_invitation" + lesson_session = testdrive_lesson(user, teacher) + UserMailer.deliveries.clear + UserMailer.student_scheduled_jamclass_invitation(lesson_session.student, "custom message", lesson_session.music_session).deliver + end + + it "student_test_drive_lesson_completed" do + @filename = "student_test_drive_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_completed(lesson).deliver + end + it "student_test_drive_done" do + @filename = "student_test_drive_lesson_done" + lesson = testdrive_lesson(user, teacher) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + lesson = testdrive_lesson(user, FactoryGirl.create(:teacher_user)) + + UserMailer.deliveries.clear + UserMailer.student_test_drive_lesson_done(lesson).deliver + end + it "teacher_lesson_completed" do + @filename = "teacher_lesson_completed" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.teacher_lesson_completed(lesson).deliver + end + end + end + + describe "InvitedSchool emails" do + before(:each) do + UserMailer.deliveries.clear + end + + after(:each) do + UserMailer.deliveries.length.should == 1 + mail = UserMailer.deliveries[0] + save_emails_to_disk(mail, @filename) + end + + + + it "invite_school_teacher" do + @filename = "invite_school_teacher" + UserMailer.invite_school_teacher(FactoryGirl.create(:school_invitation, as_teacher: true)).deliver + end + + it "invite_school_student" do + @filename = "invite_school_student" + UserMailer.invite_school_student(FactoryGirl.create(:school_invitation, as_teacher: false)).deliver end end @@ -120,11 +221,13 @@ describe "RenderMailers", :slow => true do after(:each) do BatchMailer.deliveries.length.should == 1 - mail = BatchMailer.deliveries[0] + mail = BatchMailer.deliveries[0] save_emails_to_disk(mail, @filename) end - it "daily sessions" do @filename="daily_sessions"; scheduled_batch.deliver_batch end + it "daily sessions" do + @filename="daily_sessions"; scheduled_batch.deliver_batch + end end end @@ -140,7 +243,7 @@ def save_emails_to_disk(mail, filename) email_output_dir = 'tmp/emails' FileUtils.mkdir_p(email_output_dir) unless File.directory?(email_output_dir) filename = "#{filename}.eml" - File.open(File.join(email_output_dir, filename), "w+") {|f| + File.open(File.join(email_output_dir, filename), "w+") { |f| f << mail.encoded } end \ No newline at end of file diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 7a4ab16e6..e89881616 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -5,12 +5,14 @@ require 'simplecov' require 'support/utilities' require 'support/profile' require 'support/maxmind' +require 'support/lesson_session' require 'active_record' require 'jam_db' require 'spec_db' require 'uses_temp_files' require 'resque_spec' require 'resque_failed_job_mailer' +require 'stripe_mock' # to prevent embedded resque code from forking ENV['FORK_PER_JOB'] = 'false' @@ -60,6 +62,8 @@ CarrierWave.configure do |config| config.enable_processing = false end +Stripe.api_key = "sk_test_OkjoIF7FmdjunyNsdVqJD02D" + #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' @@ -95,13 +99,13 @@ end config.before(:suite) do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) + DatabaseCleaner.clean_with(:deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] }) end config.around(:each) do |example| # set no_transaction: true as metadata on your test to use deletion strategy instead if example.metadata[:no_transaction] - DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } + DatabaseCleaner.strategy = :deletion, {pre_count: true, reset_ids:false, :except => %w[gift_card_types lesson_package_types instruments languages subjects genres icecast_server_groups jamcompany jamisp geoipblocks geoipisp geoiplocations cities regions countries generic_state spatial_ref_sys] } else DatabaseCleaner.strategy = :transaction end diff --git a/ruby/spec/support/lesson_session.rb b/ruby/spec/support/lesson_session.rb new file mode 100644 index 000000000..401dad8f4 --- /dev/null +++ b/ruby/spec/support/lesson_session.rb @@ -0,0 +1,79 @@ + +module StripeMock + class ErrorQueue + def clear + @queue = [] + end + end + + def self.clear_errors + instance.error_queue.clear if instance + client.error_queue.clear if client + end +end + +def testdrive_lesson(user, teacher, slots = nil) + + if slots.nil? + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + end + + if user.stored_credit_card == false + user.stored_credit_card = true + user.save! + end + + + booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.") + #puts "BOOKING #{booking.errors.inspect}" + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + booking.card_presumed_ok.should be_true + + if user.most_recent_test_drive_purchase.nil? + LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive) + end + + lesson.accept({message: 'Yeah I got this', slot: slots[0]}) + lesson.errors.any?.should be_false + lesson.reload + lesson.slot.should eql slots[0] + lesson.status.should eql LessonSession::STATUS_APPROVED + + lesson +end + + +def normal_lesson(user, teacher, slots = nil) + + if slots.nil? + slots = [] + slots << FactoryGirl.build(:lesson_booking_slot_single) + slots << FactoryGirl.build(:lesson_booking_slot_single) + end + + if user.stored_credit_card == false + user.stored_credit_card = true + user.save! + end + + booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60) + # puts "NORMAL BOOKING #{booking.errors.inspect}" + booking.errors.any?.should be_false + lesson = booking.lesson_sessions[0] + booking.card_presumed_ok.should be_true + + #if user.most_recent_test_drive_purchase.nil? + # LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive) + #end + + lesson.accept({message: 'Yeah I got this', slot: slots[0]}) + lesson.errors.any?.should be_false + lesson.reload + lesson.slot.should eql slots[0] + lesson.status.should eql LessonSession::STATUS_APPROVED + + lesson +end \ No newline at end of file diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 193f5ed76..ecd19635b 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -258,6 +258,45 @@ def app_config true end + def minimum_lesson_booking_hrs + 24 + end + + def lesson_stay_time + 10 + end + + def wait_time_window_threshold + 5 + end + + def lesson_together_threshold_minutes + 5 + end + def lesson_join_time_window_minutes + 5 + end + def lesson_wait_time_window_minutes + 10 + end + def wait_time_window_pct + 0.8 + end + def end_of_wait_window_forgiveness_minutes + 1 + end + + def test_drive_wait_period_year + 1 + end + + def stripe + { + :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', + :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', + :source_customer => 'cus_88Vp44SLnBWMXq' # seth@jamkazam.com in JamKazam-test account + } + end private def audiomixer_workspace_path @@ -270,7 +309,6 @@ def app_config dev_path = "#{dev_path}/audiomixer/audiomixer/audiomixerapp" dev_path if File.exist? dev_path end - end klass.new @@ -328,3 +366,54 @@ def friend(user1, user2) FactoryGirl.create(:friendship, user: user1, friend: user2) FactoryGirl.create(:friendship, user: user2, friend: user1) end + +def stripe_oauth_client + # from here in the JamKazam Test account: https://dashboard.stripe.com/account/applications/settings + client_id = "ca_88T6HlHg1NRyKFossgWIz1Tf431Jshft" + options = { + :site => 'https://connect.stripe.com', + :authorize_url => '/oauth/authorize', + :token_url => '/oauth/token' + } + + @stripe_oauth_client ||= OAuth2::Client.new(client_id, APP_CONFIG.stripe[:publishable_key], options) +end + +def stripe_account2_id + # seth+stripe+test1@jamkazam.com / jam12345 + "acct_17sCNpDcwjPgpqRL" +end + def stripe_account1_id + # seth+stripe1@jamkazam.com / jam123 +=begin + curl -X POST https://connect.stripe.com/oauth/token \ +-d client_secret=sk_test_OkjoIF7FmdjunyNsdVqJD02D \ +-d code=ac_88U1TDwBgao4I3uYyyFEO3pVbbEed6tm \ +-d grant_type=authorization_code + +# { + "access_token": "sk_test_q8WZbdQXt7RGRkBR0fhgohG6", + "livemode": false, + "refresh_token": "rt_88U3csV42HtY1P1Cd9KU2GCez3wixgsHtIHaQbeeu1dXVWo9", + "token_type": "bearer", + "stripe_publishable_key": "pk_test_s1YZDczylyRUvhAGeVhxqznp", + "stripe_user_id": "acct_17sCNpDcwjPgpqRL", + "scope": "read_write" +} +=end + + + "acct_17sCEyH8FcKpNSnR" + +end + +def create_stripe_token(exp_month = 2017) + Stripe::Token.create( + :card => { + :number => "4111111111111111", + :exp_month => 2, + :exp_year => exp_month, + :cvc => "314" + }, + ).id +end diff --git a/web/Gemfile b/web/Gemfile index e4e4295d9..d21211101 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -46,6 +46,7 @@ gem 'omniauth' gem 'omniauth-facebook' gem 'omniauth-twitter' gem 'omniauth-google-oauth2' +gem 'omniauth-stripe-connect' gem 'google-api-client' #, '0.7.1' #gem 'google-api-omniauth' #, '0.1.1' gem 'signet', '0.5.0' @@ -59,7 +60,6 @@ gem 'carmen' gem 'carrierwave', '0.9.0' gem 'carrierwave_direct' gem 'fog' -gem 'jquery-payment-rails' gem 'haml-rails' gem 'unf' #optional fog dependency gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) @@ -89,12 +89,15 @@ gem 'htmlentities' gem 'sanitize' gem 'recurly' #gem 'guard', '2.7.3' -gem 'influxdb', '0.1.8' -gem 'influxdb-rails', '0.1.10' +gem 'influxdb' #, '0.1.8' +gem 'influxdb-rails'# , '0.1.10' gem 'sitemap_generator' gem 'bower-rails', "~> 0.9.2" gem 'react-rails', '~> 1.0' gem 'sendgrid_toolkit', '>= 1.1.1' +gem 'stripe' +gem 'zip-codes' +gem 'email_validator' #gem "browserify-rails", "~> 0.7" source 'https://rails-assets.org' do diff --git a/web/app/assets/images/content/stripe-connect-blue-on-dark.png b/web/app/assets/images/content/stripe-connect-blue-on-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..aec105611cc51a77ac463e469024837f8aa3409b GIT binary patch literal 3378 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
*K}7LTfi7KyW{s`<+Oq7glLpmpk^ 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 z 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%0DZIo)I#^{=#Old z6oBTiygYk)5GI1#3ADUta_P7*oD--svaMN=kpho4kdU3q4hn~Y=r?*Q4=OdcXn7XE z=>j?pgcR`2&S;BxXy6$`>YVysw~3p)#N4O=M)Mft-8-_-lQrAVaPQFI49ql{f{BK& z=U|@*jjp3w`)my)TbD*OxF~d1PE>1dSlv1rBwjtzYr9pNL{#YyO=?Knq>CJ_t&g>K zuMD@6b1All7@C%z`#m81?u-ZDpx64~NqXM|1Ku;F;cGAQm>9kHwATD481r~q?5i5f zcbfrzZNT;Z%*?k!>EEc{XxS~-dP9LUXoi>nYLV-m_E|gSg0`|Ob zZV(+YMkl2bYg&(ox)kD2smp^9X$ajQ#X5HNq`mQ3a8%O5Xo0|ofrDTK;mcFC10-;G z4)(!yCOsr6rKfwC`^$6CgWy-oSis8cpw!?nEc-4Lnk<5wwF7excBYt6%Htfhom6J? zG?}4;uw9>CAi-F(zzj|*j3T&Y*I{C5TdbZJh+;r=YQqt$QFu6my^HdqJ{*qsw|Rb$ zHx<}hkp!Gwua$+#0l-$?roR!aQs93h$F@u`O}#%MkmE|~T?{>7F&BD6sAC{11o}zi zJWt0wo&shS2@!?1jL)A#UhkydI)+}5Sk@G7tgq!nbtKF)!CDntZp*dE$)1Av0Q*`d-7RbM$7O6DMS0dbD35b@|$-f28D81J3Aha!_A>P^yPSxQtax< z6(oVn0w)oO)BrG+7vq$awG1}(@x`sIgmfCEBp;3WzdwI*pXo59?hKG~6_wD?3j&S) zpgdC2AwiCFRzu8vODoX$m#Q6m3=iz8#O+KL!^54dgAjL8WMlqJ`R{oD0NgIct(^g;43VsCPJsz!QrRw! zvoRb=N;48s2iC&~qoEfDYfUPmky(@Eb^zP255tbOL>LCi(neJaN;fbJ7<9}jVR~t4 z0}d0jO-|Dkt7`uiO=AEp3|A%d6t zaliM&baG6R!P-uk#daMV0jbi3n40VW2KiyP#|>~wgJbH0#*ky1^+llz-N#(?%7c!3 zK&P_USuV#S+sy1ChG*BZGdnm)XY~8{K!xjV0Mlhb(ogNlgwC=Z6*zn*a%8DG27AWR z#}yl1QLhp?s~I^K%*Ktxj0|)Ho1}2J$-LbbwQegX|gdlrQQwlz4>#!Ed+XJLF4;aalZ45ztw>~{#w7oMf}#0=G(fq zz5#_cpr)o>uZ^Q`HP-V?xG!ru0z#YIB9cIojWGA10@E^Q{?-ULd0$dmw$)8ab$Ka z4C*w!nI7C23BcMTwxMS=)5DY(W~1fehhRD>}+dH_vs8nek$zKa1$z_HXQW#Sx- z>JZ>Z2zi$md@|$lxXYbXcrf<^9T88g1?oikRu?-xhdIrHPy{5U(@6?KMmuHnUj?iA zh+=(OP|kH-=J`Pi#_?qPLMB_0Qy^!jPD^)vBY3KY!Tx4!wjAdM;M050slaCyRy%ZsG0MiLs^8&qt zfU#Vr(>jngjnwg^UA?PD=Id~y=@_+43gPoC_4_XnS%4wjXdbz95mUNF!D z`ca70;Pljx8_yF$6jE}SCwiVpq_BI8t&x=~a434E%Il-rb0kqU-NR~t0B}Cz)-doA zth`Wkt+5_7#*i(D-ji$}KtEUPw6W}IxPieJew2ZOg>3>5a8njUIwvw}9aj?wioOVv zfZ0@q!Lh7iSPXkjmV=;E{fZqk0gUFS6G0$)T=pNmYi4rXX$cFKHEpGfE))8sAR(P$ zB6FUnhdkW@3L%l0zKev*&Jlt!H7G!-xfXcFZ;!(&5T{QU3&BV2*ksoP(F z?4LbzpMM2!)L_fBfJBRMKI1iJ5e7AJxZ1%M`u3iIKp zH{f4{b1BH4uWw`cRbb#$0H>{Lj|#E!Zdhi-Es?G%~DjWEqXcq5Cph@7>NzA~ZK$YrG{G?U}+ z0pWLPLildr;hUy6Z^)p3%wfDIka!gUyeFi5Tf3bPcwOEn4)Hx-@I7DT4VExp;|ZH@ z^8xxMr_(o#wO=KdKIXSxeSPyfEa=PyY4l8|--d{ds%09Wgg={Wgw!U@ZY-uVf>d=6 zNG*Pg2BIJ$TzJHXwf@5}FHzR7RMut&4~tL)@zT(}kDVZ<&Ubx4a$K?;78SvA@KAww z<`8-*plM4lkNMo{QVQ_dVTBo}K?Z>Hc$!LyuNsu)q><;eB3h?R5IdPPNRr@aV2f;#naHQmw#yK62@=_UHc&OLpy7c=2 zxN<+pIo%;!4`=Ii`#i=$ez&AWK4s|oB#`ffs#iXE#RLT7OIi^orXWU~9Xe`|LpQqI z(#YhH45+r#a8<|C!@Mx|(8e?+9w-V}Daf)C%Md!!laE`5+M;e<>dGaW>N; zH8S*LH-|Eh-LRWq+|Catf*%|b_*&yU-yNCvh_-#$^(ocPN3DjLMh<09PUAeLFMo5F zp--#Yvcnr5y-BNCn3X}q~CUOFmn_pQA!gldf)rmxqm%qq^bFN&nLHK+lRhO=uh zfkvROT_2-FJMdIzvgYcR_*FHykM1G0G;@*_uU@hh=qIQgf{vIqH8*9mW-VgMm)e#^ zBUEB6f+Q4knqxNStuj>V7+xXjO}^A|THt&X5MJtB#AcP|3}OLnljKU}v~7q*H&uLa z^j6T3kX-~xlAb=uSl9w5;5+L0U7oh=Xxq<#G|SnK#-J*kLi{=~c>+KHgF0o+fWdx5 zIxh21y;2P_ZU>FF&!kY&ZUy%~(>1}#C}*C@+0RCAih0(1rA!DYN31^UU+ z3Mj10aym|G%sZdP$_`&bm7cbDCA^#E{U<_A4ng@7- zWin;BzD7*nG=aP9XyY1j_I1rcE{eOH&PDPpSiBqJP;44-C4G@r$XPN1IB zOV$(X27t`2b)+D_!}vResm5W*p|fd$NF2H9#v*qQAlI@P$E1ug zk!H!9X8FO&vkrkv0nd|;x$r?F5_o5$g@(q2e2%1AK$0rqZw!5;ntIk59DaEdOzD`4 zONXrAQ>zhH8QId~q zWFXtwrt4+@i#v(cWP3-lrn9QKCA`j1Ad{ty-U2O&6$G}?v88mRZ8gm-oo6ibMr2xv z0!Ulx#(}bzUFvhU+t!QkU$EHKuxZ`p8qd5@D6|(=&tUO;K=>V+5dO9y%44QIkHLZ8 zIvRRRX2u6kN_c~e@Ks>*&SCMtrAzR(XX&lr`nUSTJSGO?-5L<@u-@z{s%hEMW{ZqJ z&k60hBCJ~y#6>)zUDJMQ%ZqQMeWzoGtv_Gbvh!yh;zcrsT|FN*6iGEfZ3d>Um7?m1 zZ<<}chC63=oCC81`5-FrAaQCc!XUw{lOm4tVDAaC6a7sV!EJDnLp!w5R5|xcfh|PX zCeWK-UVF~TGb&85UO-k{H=d4goxwv`-U!IAunS8yJ&a02j(JrUPyuLFUIkhbI+d)H zQP{2f%v6QbA zZDN+f6tB=LB@QO^z8C!HF)GAZEJLp}$r-ET@%HZdZ-#!@fA$oGZ|tOiliw-wfhMvd zyNLTiUSygR$y9&}aO;t)EH&P4z8~FS@%k9u8F__8KH4}zUV7N${9`7PDS_mqeEZmVQ}_0`>!;OeNd7jJcKW9m~HDY->8XeXT(wqSz>M%^V9 z;Ap%wyXx?BRQZL~*RZP|hwQX9v4*uSzNPf<1TR9PS?nz_VZHm?9qcY-9cE2BZDL}T z88bnVn~!t4Rkegfd4ot}r)({TTYvP^gp5nS?=ksE%NTq_$xV%&TtZ|WV2kqvyp{L2T8{v97DZjbQ5u)KZVxM3 zBe> z(u4+2Rw}cA7Br7gz8&fDj7e}h&e$GeXJ996K`V_!mI*Mzkw?lc4P1f?D`_+ktf4;) z_}&_9SnK9M&8nRru-X{PJQ*4!r+LOIKrXASG!V-{FoXeYb-Q@Fdk`3yrrGPsJrzI? zYU-TNWrkxZb4F;~{NmVs_F4S+i}agc>YxhObK!={BSD(OQiuH$*|Z<-1veUmf-bOH z)^25>ZqOrmoxPDPz_4^X!Glvbk1&o!^Aa$ST1jLMx|DDTU}vk_5_VR()DsAnf@wO; z`!FJ`yCGx&Nc(*r4nC)02j~@eDZy!B6d%L|BoPoy!}L0O)614409L?b;Sv`lC)X*! z38{cP?}q_&0TWyVXN4(xz#(;zCsZ2MvpJHGJr~t<(Az1iaj)svzLnL#FilXljsXt^ z+^Vqrv2_T9WyGqPaWyU=6t&KY3RF1<1CJE*Uo@>V?Vl~X|D9f~CJc7V*5%1x}` zpqqoEY}RR47!UNsj}p+!!YEoDtzjcoO(Me^Gs4CiAndlyS12_ryEdt~-|5Hkiwp<}F6ceGdq~D}P!r?uSMc9}wHT zfuZ_tI>5)^#^ga1QpcVU@r{-~{M4jGfO~;$QgC_+$N{9< z1)Wl&*IEb@4LPgXEZR1eGC~tCfTYkX&CSwmnjx9XGapE*l+l=aJiB}!r3RbtPHuqL zpFAAXBtN?lD+;=qV2_Fs9DVBVCp=?~Adu`1*A}S+$v1+V6=b1LS9CxZ6-sn?@S=kX zj%tW|d70@1UXz1efC*jT(^(Ko02W?mLnHTL*wrNqoOHt#S!uJJG%4($V_8yM7PkMZ zVBFJuUr;0o0_y;AfzK@Hy3ul80lXlvFrCKf_z-sRNUt!&hrK{uP-dxVolalpyuhFU z*F%c0S)(hV&hfzr@>ymSCYC{Nkt|aLbSDf4N~Wd8;!>aD$3;GhuROdE|@z?{cGy+#dE1)LwBeMMaSn8&}?F=C-_VXuoJAe zOiP8qE*#`5!L8nZtvBC>d8Dx7RLZM;P3i054Yt)-9 z1F_bZ$PIG0POP0K20*J~p;k54vFqCB&o}{+2on!Dc}Z(XtKX=oW;g84x%uUc$7tK4B6L8)z&7_HcBnh&L(dPo!L(c0gfA=9!xc(uRQD z9b>Y}8{HEbl69;pMYZ?W2vFSRa@BWru5u=F&X`Ee90??rQBfLkiHL`$87Jxu<#eFR z=1Eri`iLA^9k zG$<;}MCO1|YQ8NaJx_|oI78S56FA^D*n94qK;wq^x@_o*AXm7oJI8LkI<=}+L<$Q( zMMSOdt?XMsdTO;eoh>@Huv-7ov;@hMI*3wGO9f1bRZ3S^%($Q3#fL{ET^0Sw!9yRO^w z*4p4q$@z|RSRR(k=kKXGaT8Q+Ay#sZfUNl zGelJhN{NYjIP))EA8KJ8C~%m>LTW(F;l_9Wd{%I3@PB|l<#Ew|#vQbQK5Rb?yZbuJ z?kXqjR+Fh;W5TTJ*~W%51VRcphf7Q!<}u?bG*rri+8Xpeck1I$To9b#pvlMo;h+BF zKmPCkXgXoC4L)hvFjI?x^jnb9bstjyQ+mcQ#1lxVdJgTNGxR4$HZ<9g=p0#Ks6huB zz(3sno5q9?l48`x6u_cnI}#xkEyJ0F`;+CFhUG3;pG;9S+F?P;JM2l76l^em%W8Om zA&e$cz||xom=LsvPI72*%xwiWVr(cvSwW_b=y^3JW6%_tP}->~P3|IiP&TC*IHp?G z+>k?K)%z6OQU}@q6Hb8{F{Q`$2byUcngqGu1Q{0CBbW8I78+8?Ppy0D444Plg1{^( zk072HT~yq;%4VFOzuZng{^tC9zuW%u-(W%ObQNkmxg`aXq0zaVLT=lamzPON1WFyo zHx0D0#Ecm5jP_&l8G*!CZ?5n1@9SFeOc2m6 zn}6*mY_N=2X99~nrof>!va;w_>wJ-?QAJej!X5?|XNGKJ=G@Fv_+FAMQECoxC(0%f zn$FWa{)dSQ7{5cs*n1=^THs-YTes}%)$xfwIiGpI&dvdNy0JwJ5ej~*hND{?RAQ`+ z8DL_i9wSe}RvN~b@$iRAzLEHXfi5-1$1M6745(^&O#Zq(5;i#YtHgEfHC@8l{#MBX z>{u;Fl=s-JHh> z|0nM+{6}PO{Jng;f9EIu($%o{$euAne2Wr)!C3~6h_~*eMopWkDDwR^tGd$Z+rH+v zzV5Y}_Iuw}?+6ny73`6#m=I+XpB5Oq;Hy;cGy+QP4Z1eVHL(GO8jZAb&G6qefmRjc z$y7;7f#9ob%PIB>M{uX|6MjSa3pn=yJqEbCfj(#U9V#+GJ9x9K_VR7J7Ui%SWJY1I zgQtciC$pPelg|we*(h z2$!?axD`0ng8&<1=jaAQ%h?;`;T6j8XMu!8%Bdb3OcplvF6fEsqKI)N2(I!~JGF&0 zAeAyfez|TN(6iK;r3ffO9X9z2@lp|7Bv2DZpo(MR&}VBd`*oj}AfQ(cJz{_r=vJD2 z50zytp7bmZF~KN}MU=xx&UvYL3V>=d9KE&`z+Orq_{_}byl(<@-Ey`uq%U6tbIM5! zC;73xt_kEb6X2GYpsex{q)ox7-iMl*c`;4!_9;a!O*RE&R7jc7kKXsytSJS7SMsGN zRM7$j1^Ih4@;S7EtPAodI6S!Rw$D$~a(Ya+>+S3F6G?$ znwIV53a`E;oX#^Cw&<;ctBe6*N`eZFTm`e>gwRf~m4tjQlYNYc3#acLJoFnuUg@`& z7Zd32DwpPVLuR*>kEPBI9*S}y!;C;O3@g$#s|_Wl1Q;FEk*?5{Po<&ZI<}cf#FTGb zj*wDyRzFM@tOil&@Q~#%E`DltY!p`A`wyb0Iha;QKfWK%QX}};jXKQgDei-4s-V>B zWXfP``9bz9c zn;d+z$=Od+aA|iZ;G*aq(3l1E zK>)@DT&cr7m3untmD`Jv25f+guYj;fkl1zBfs!0|6G(tfJ&|R5$;Rr>SL*qy05yzL zART0ag=b_Qsj`;UJV6-}Bf>y-RV`Kw8NFN_z{f7-!Yk*=`42F5!BqpRQr;_IQpcQg zPqVM4v{pGh*{Nfvb1WR5d8P>)Gi!Lu+n|7Fo|{zxTNsx!%rk6y6KErHN53GdhFUO^ zSv*sM?1~te#k|Y-OQ!kNA2p{av3t=WrAqC?f^&6X>mR+z6?Hv3H4wwKJsia^}rr zh?%TmAnoA0y2Hx;C?A1M6-O^03KJ|r)s$04c));1gKnO~$~Xs{MmoV_m#CiIG*EK^ zTIFFvy?dv*4F-0&eT+H-(M5-C3^kPs*o$SOLLeb&*aE^#P)lR=WF;RLO$8eGD7kYu zL%Cs3g$2Mc97kR(h_|})aGHxFR2xzf!P_8+9>mR2V_TIG)Xzc4wfBL#=B6^9!$|;J=5N#I==(LU#HivEJFQnAyxj0LC!zWTy04S0sDL35h z`8m!JjKyg&6(5P(4oC$M5#PT>dUwFytq`FHnic}W38rQbxD*y8$h`mvG^FCR>*o6u z&JS>|-`DP9`0&K)Mvk%hh>B`oZ=3)K(w>N&)JDT6?|TL|6MoB>&I+vjKbbG-L7%i-Q`Uj?Vm=eg3j0!fcW&J_q`esaaz+w%nu z6!ODUgey-?$mep(z!K1(TJGy@jT=6?*^?|WPjB>+R+z#I6I($l*^&yzFd*4d#0-W81u#VQHhDVzL1hxdS-k<*Umo&`W9Ww#iG2v%0cqcdq4kZpjq-$oV_gEaU$a6{_#ZJM*i??3xD$_gKgqnlL zAG5DUR2q=jMo1nf00Xm*vhN6(utq0t^li~epgJU~%jp4d`#q&zqvWl+jq($UZPcv- z3Skz}?MbfeUSQqC+I-t@lT(2-{D?s{2#^utPGr<*XU|3eJ3Wp)Oc>Q*)A_?K89NNX zgQ;@E^^{Z(CAB*4S>m~_0g;N?sBFUh25+rQh!ish-+i!5Nn`AXWRCz3{VaH3$SzKt8Uh;JsA$2triniYnBm+KEZI)~RXS}EB|zl0q^C}TUeqi*nh}l(Tg%hKQp)zc-A){P zbLn8DDwtp1!Gbb6yluO0yYuIVdAmKM@!=!&%q@&K5_GmG^X!oh!f`V2oa$zdOR71> zAn?oIEo`C8RHh4rl5>!ox9*w#uTTYo9>5zjuz6VM>FU}5qO{FyU9E?_tQWzNMi3mb zGB$a_5VukE=hTmdx$Jl@(x@3RMrZ)EDhFTLcA!%au9&yk1CxXQqU05NlEPrE9tplp zWf6p=Ma@f{)9?~5HV~0HX2oipNH67QXbS+VJz$Q9ZUc0w^xjIX+GC3dZdkymsz z4>Yt=GK+Db_z&?OZu+YHF_Oq@X-8g_X?OqOf!Rhp{UHPFM;wH$!@YbIHRUK<`shWC zovI#n5@4t%nHu{_qIm6Iziwykt={k*5Wc_OU+=G9?>Y8sb8H~LS9#$Gk?)87gCTf3 zbb$7iK&lf6=#&QaVz|)Q0WOtVHRhy8@pz|>jfNl6&HRYqD>|>hLkrJ=N`(|jJW33M zl>%>}1T5;y#-!lBSEx{0bojpn#ht~NvB%RqpO&%pS6ftR&|DVguJUuvK+DH@cO&+FoLJB-Ht>L1Xrl&K?%6-KH8z(KmSw!a{ z(OVYeFF4op(~RH*Xa$kg;Z;orMaxug8+=Qp2=+lhpm$;0cEOw_ zZA@VRV>LXU8pN1&o~HeB#R0?5QwkFtm|P(B1tFVLzM*Yv#s5mhmfPbuPx2jaU!P@# zEFV5tRIHnFkB%qhY*rl-fp;EK@+7-KfjM={ZX@QGk(c;HGbs&Tq^BN zIHnO?5Q!onCiLV#jMN|b;_--H06Hxobwlmi-~{DB%Mg^3He{AE(xGvZ@3pN5pIIBb zN=QND&-iS*854>ZqfC6C;4BDi@g z&2x(+d9k;-Wcs0Au65t1$8XmC)}Ma-U**5}XiVX-wZRf0uXPXN0aIVqJD- z&v~3ax2mdHF}20edy;L46ELzRNk;Y*xj~i5Tn*Uk&G1=oBS*00(^W@NyO9DD68xr!4#nz%mEcDH8WRc4Car6*E>b+oNv=z`^ zfM`$_cEb3abH6<&dQ&-m9(chR8OCfV!B972@Jn$+fl&(OBCf+?T{0Rej02p4AW1Jb zgY0db?X$VcCW*3t?VUq@Ux2o&zMUh2oMzIC7BE@j+1AbfwlzusTJ&SUex5hA|g!vXBf zTh8aegKwCgs-1KC^Mj6m07#=w6^EaM)4Xf-H}qEb?O{gGx={OtOf_^PDNzOJ{*b=G zI*-FSXMOD@_CQuiQ9A)C;B1xhBB-0U05LpBVcghCq6BEcG%v}I6^3=u``xnBs>Y@h z)Liy=@5gx6tS zuQ;lb!oyjC3k(<6o9B3`Ny#&{>$W*It&yT;;a=0Ll)J0R=m-?JO2`{Zc`xPNykAx$ z=VWcCCMk5}8s%KH%K>2)oc^&=>fVcCVxbo(ohX$7tJU}*i!QAf z!Rseln+91+w88Hw6C}9PdCAvZPF_Hqom9PfKAl|8`@VA6(CB4n5%#@7(piyk0&hm7 ze!C!`{I;fVzrK98rlMzIxN2$=2H0;#!wsk=yvK`@20BJ|qZ3a3Sp7cRU*$c+nvb)@ zP9LT_rBnZWG>W|Dh#7N7ok|dO_8^+~QJA~KnC93wUYqepB3Wb5$esKE4W5NfPN{vP z#bIy5>AsD*BB|hfKlm4Ua>!I+NSvxk6ZB#9AP#`Wk7jpG`_0tNOO}DunptK!}`l{ zUEz>?Cp;iHgFnue@iTe(^wIb!^=##rWiz?RN6M|4t&Z4qTZ1sz_nc;jKnI`TDty_} z(^~p(e|r3b-+%ePzE&jIyBbMg1zrl_^k8IL$+riP#;aGv!mrL0b#i+#97XGM~<6Oo0_E`autx zs4HifIrHRXQum6b=qh>K48)WxS1^Y~_(`CBFjhgt{+|GjFlS@xA+DgRJ29)pqHvVj zf$#!DuF=YE+G=qm(4gZP9P$TYW*q2P(szJAJCZDWdHAgz)|K{(67?{{bQ%_A&jwLEd9w-|#3j8X07zO7LbV+jnE;(9K;s zIIO^i$sl`tWEL^niiFfL7C?IuFd6RM0(+m_Z(zx|N_em5vC za9ru@O`wjPpwr`n6n~AW!iBymF4c@Tg?3S}+^J5!6eX~?_TPd+XNu{%84+A_@ z#PYqUhLUPo5GE>t?Q(;!&*Wgnzva3?G3Du`cz&GsmpyMSmsJp{+5Eo2pW)%*L5lBf z-(#JD?2rfM*}ccqaldUSH|3f>C!`v>aC^yH&r*(JZpCnIhGei!-6KvTnhHb~q3_q! z_B1`jDatS2q&AzDxL)>BkhqwxD=3PfdE$1d%D-Oo^?DQFcwQcrR4g}55^V2Wf^i^< zL_mEdX0eJqazk~XXAj+=M)=)10I!UwyZ*QzH?}(I>IQN5tC36K4@1$>Ez;fu#{q4m z0}j1mxCee&hNHYDehud8m@0NnS*L%;p*PgNJX;iG5&9$Yo)}!WAm)#o{w8fqw;Am> zjXrg)yG^J(^)il}Mp=)QDMQxP!K(Re~GySGQis%8h)JbaZL z6BTgyd9v(GwM?QQTy*M(=0n0UNewkBnX?!P;#xs-hzTL9b$shw)dU4& zq^1dmn1!HC1D9jDq9Jy*Q-UdS)oX}d5gQ}u?9`UV5t^aNKHyKlIno!>TV;m0ikd0p_ zj{JJqAHXaHMFfmljJIVq*UgrlW4`9MO_o`aPfb^YE+XcQALKc;Wo%#(V?K~MR6lBu z2c}%dZ663-(0XUgWk5I#9%bMU7b}`Cg++*E;|N|r6>`kXpcC|!{nK! zs}9*6jjRcv_Q<%UdYWM!Y-*ARqQ@Xu8xaWnDa_FU2!`}wqXq=%SqV8Mzop z71jY=vjr3r2o1X)LF0Iiy&mM;7^9tEU|cd(aaTcXAS;Td=6R$I<1M45(XPsis}k4Q zJTAEDM;!2w5H@p!UuoBge!RNp_}BC+P6*rg5S#`cj)WU_Ow7^y?)T52!CoJm+MDEw z{!2a#%5r}PgzvBS*Zb=?8y(uiP-}R9-6`|^=;iehXRm2L8sz%DbbF1U)y}ayydA7E zcC_)vf)|wvl^*;Dd(e7DtTyW0-X}aPh#vSkfJpR4$lCDtj$wRK{J~Eh?OGHef=#Ms zm6E^3jUKy5pjq)JzoJ-1jH=RW7g7xaw+9-yT+k4KgmT1lK^yqd^GhTtFFUo zaSCM9S&oOUZ!AsB>?VJXB|q$&#r`4-Oz-4^i1Q68pp(a&LC zq@<0=8S`n~A2S$sNIuXh;gIR3aWbtiMWy;S!zepkLPY)+`S7!xgyC25jSK9r)si1MUyV9VYSOeTYXM$)P=oMNuRa7x6`nw2@$4JOg>66!iy5!{4eZ;TP? zcHA`&fW+2mim*hIZT2+bbg4-R(_!WKak4Q@V$Y=`-V;$G*pCEsBIAZk0(?QXtK<%> zPzy)USuKoEh@JCD>xh2Am({5!SAn2A^?7YfOXd1#8WYLhT%Z;;d$_SCWEH^C3tKZb zWcZdqEDdj9=aw`R!L{gg5j+j6;7OQXkYMlnL?Qp6>Y=%?27d3<% zECLdTaS1sEheUhW8L%9{)`8*SNWM7Q(E71D$!o_}b^HT~wbqR&QPT(2$>=8I%xl8L zTf2ns`7pRq67_dL_-j(u{(m?<;{Emh`qjUh8*;(>Km3iaVa8&9$$92TF0?~*edI;h z!w81XV#aD8U(2+6y`~$qz0yP%b`sr zXL)bTf8|0o)=mUoB#G|XEH#8ZK^R$MiK+j3BL5)>S7S2=4bK#phy_*pl}7t6Ul zo|X(YnBa9l-?kARQJF>mpptujbo8Sra<0&lzOFa9&JT0!k(tdfL&EtlDbJ5ff-x-S zdGagrC$NTXJ5Cb}IP89_*B8OAKCc@F1?-FzMt#ufQ{FGgS2xIJ1WgLp>bA+*e0rSM zTfN|&WO9;vr{rAY3dLQq<`Ub0i zDN17JUWuMyz zUW{&$ts5A-=QD@Xa^EJ+m@q20cEj?kG@^MnucruBx2MC8)C%Y z%#D7>1d{T*vu#tt30!TQtK9r(SK&@acWDfD$$%v>mi$lKh82i`OHJh$EMxcu?U%jI;Q&!?rF&eIQnXZhWK^ztwMrCi8DCo-fz#?!h@f|}iI zR47QKYW)N#XbNSSO$W{LTOm$QPA^02^Td`?sqp!Nq5umecu0=*vI0z!(L@j~eoc!Bv=w%2xf+|F61_;wr_XY(ZFc zaY399p)0AUDpR$@&9!J0j5G8TYSn1yB`z^%zn0Hzv~`si05&z&5PCm-n9$j-34@;g z))cSern7i(%Z;?aBpf{#RqpAm4m@dg)~ZJ1z|_>VL$mxE8?PJ){->G4?%43Fk|KPN z$wyGl$c-6$*c(SYqd$!y?Hl0N-)Lq!FKBy1O&{T_?||_A_5ON){U#7Z``^SDwe1)Z z4sz6e6fkU@yRr5cO-nVYRe|3zKs-c5cJMj|%Y$1ko#pRw@s2U`WQ(BFXh1|lZ=P(> z(&4;VD76O#RB8(O*J2hV=knva%V63m+0!v$p}}*F9)LT{78;%|j07|$3Os?|3HU;0 ziDBQ%D%A#}m8{t^VtW%ZqoUORcI$#;vH%t|B_nJ>tA33N_KTM~CL`7&I|P+AkXd`1 z+%&P(F2C|}xdaGwkj5kk#g^)3s1>EaR{>bh0#W4e<*A-oR5NM=>J$N{bTW-dJ1}ZX0Ue1ZKS6MbukYtLr`GdcAu_QxW%+U? zIJzlrsCDNFBUpM^8vpUZM>vG0OF|7Ik4(=jY{T|)OS@pmAP6Pol*-qy)A=j~$aMC7 zb7>_{%(+QwR?^jCT()h!{`|{_-}x;k5Yf~STMNiaR>yV?#Itmf=Xec$Y$+Q+?6ROC z;fq_dZ`XXev}>N8p5#hPVG08b6t%XzR73_0o037NLWvepP3_OOd6~vc42z;|YmRck zPMFJfIP7h?v{w74}oJOxf--`fC;M2 zU5fG>WGcFiC|x0`&mTiWJ6!LTTUw!6%dS3IP8P!y?>Q$K=)0+O7S2+NPmT@wKD!vVhhN_ErG9Nc#R!hOG}9j3LY&iFj>Odfo~^jVAvp8bw`xd z>>-zeDK0cuh*_ijh(*l?$hdrOUs}x$5;!fpo1|@XW zZXL2o!K0Anf|CTzaDlrplGiKua_q%|a_0$RV8atl=@{6JX}#fKx2^7LpI{9IVrsCm zQRJuTWS;U3yYRm54@(rhi^O_&7zr^#NcN{&X|voW7yD(oE#Bi(?T5#I{DLmr%(OtNc5usV30XmqGwcB68S6cf(> zbD(zRcldVSK}MsfjXYD`XW9V{InCCeRTOy`%^b7_<-d~@#J<^UOZ~N8I-1?>9T2|1 z-e2#p-xOrn-+;qAGHhC#YpwS^=KOsF)d+ffPvgC2Dg57J1{{i@sgxet5yJ% z6hjq^FZIGSY0q%6V->Z3DRl=|>Y5FV1O+$V_GMn=IW&i5yKW{lft^>hkLyENz!8MG zXvUvzj(YFkAi z*dgeVlBIwyMXL)8%%s|PzUT?YbQ65MH0&5FwTFCt)bsDtI z^XT-hx62&QQnO5cmX8XIgNNBn2GtS7p=l}bt^_!diX+|9`E*jn$23pcuq6Yj(y<4l z7dgVRr+9t@KQh~L-CS(}RYS&T6s-cpcPBU%Hh@n-W$RXM#XlgVs@-7!+ER;jMVIf0 zgbT*W!Pe3S`JVZ5dlX#i=XAT+3A2v;_0l#0afPa)eS;r|U*D^maL`BVNVwoU(Y?)e z*3!!!5@q9F?fi7QI7B@472yt>DmNNT{g}p&%}8M=MMq~I4!{!2G{TuvId_gsrSRMoq+IeylV!gGrAem$i3b9Bn?m>6!scU&0d~gE)kwSvaQ7*efs#v z|J~33_kV8oudrgV)06BGt8Bepy>NAHL48oG_k+Xil%I@;h#bNqI4n5u8lWshxVzPu zU4cf+B%l#FKiij#hZc`RPFkF+iNJ);YSakUT~bUb?@*bQ%Y-G_TEoNuKtcP+DuE;b zxse26@eCCxnC(m=rNR!%MmduSyE2@sLW~Hg>^g;GFdvq%&SD#!W+#IKvAkW``|HrI zTN~lyW0{-;-z5Ek*7QkBoUilKK2Ay>=Cg)~6A|wYUCYM&B)QD-c-kufl}8h_-Y|d} zQKYS@>Js~oW1~ky6EzwWluzcZG@ORUpi*VkV}CAG^VcD`JY<($dFaaX)Wcn`$HM_&1^d8cDZ=f^pa)jBu1HxaA z*FQus`upqs^_w~^{KedzVE}5rzZo9sgjNX{`tt`{>aeF!LqQULw4As*(b(5Xgt~;` zQLXU;Enm8-nM4u){>DzB-Gnx>K^@+(e~|+@f3XA_BHL5 z%6oi8DZnwAEGByOUSqN@{2@kA2A`)W2r%H2^3bz04yp`%Q1HAhyHqc5=|-zJGSLre z8syE-b7gnGHTmK@%1RiTla07UsR#<<5YB~(1vnh3Pz4_6j3X;Ffq){m<5^%2j!xh%?vu1K#H7bZqejR2241;{BZcW7VLywqX5Sp+6q>NtW zd)Mg<>x?4kaLWk25Uji>8uJ7Vr$+UGx}-7%1dfIIva5?++v^C5OAR>?8<2{8FW;&3 zz#!M=%O%-8&e2ddDu6gpRf;UZTBX*7I1|w!(5Xpnis2?pl%}|pR(#GyjUm6S;70}c z`~<(3_;`M~d=aem&HPc*)M_xKK>-s5IqjHQ*L5Q>>EwX+a=R+;(r#&ec!)#|7;H;b zYpa}F&+5(h%(;-PmfH>vDm)kME(>Shzxe^?k{OGr11`!Z?=5ZV;o})FHPwBW)n;~| zWc7ag?dNIsu!)tzI$&g+r z?8Y?zZGIrPfDvmjHUf8#Ce>(%evFW~=$%#icQYdzP{)U=CmqAVdw%_}D%S7`<*L6) zkiTP8*xC1E>}NFHejvJb#DdjVM;WlojsmYKXvKUU0W9~yY1gbq*~ZS(^x^R|1?Z4v zIpK^&&IdqFcJ_Qm(H zIX&DSQ{J|1f4rtI*G+EdSOlW?VOc^Yf_%BP=CGN`Eibp-LGUa)bo}8T{ue*~XZg?m z=l`{)F4*Y@AD-IJZ{T9MGZJ#J$2iSr4@?BXUIyuKm%FRXVLrul+h9k6sEk6{+IUdO z=WZ`hE1Vw!$th|>>pQ1p%1*`!Zw=ge0l=y5Sf%E$azRiEmJniZ zvDD;kFu~TAlT}mE&K4w_J*_i;6@(JpV!)L)Rj`88%+)Bkq-3;Wpo^Rqv?e-5jBeOG zeP7jL`#_rXQyp=sR)3830C_YTEO8jbR4Pr&ow9z&iAO+US2>|Q`q!?%37pWzwY39V z+9Q$GyaU2tn@aaxL-_uBfBiz2Jv%kPJlAxUE5xbaEgyWk|XpJi*wf8Wv&E zQo|MntC$wicXsk@jvhFxr#`_kMGzDULMceV`mq$nlj6E(vpv7CABb<1_Y7}rP|Yw> zFnmm1%j)~usJGI%-vZ@bf!+q3Aur{o;2wwUF?8!S&uw2Sz?s(< zm>{5phy2Y!iAN^}93(oM1YaR-yVQFA48x2(@?KBIu>*Z$Pzazy^ zi7~QM+nIk|CLh7j6M!f1M~T`;NU=?6HF>)r2wAQ!L4+g9!}7P zV5*@3RFgqAh0my|(pdMRt@GyPvd^eIpeVooDR zTcEH*Fot~YX~D`$`-XIl7%R#k7zRN~=A?CGvI=(915pETr0(^DL)-Uvt`E8(*!vrS!6C6QUmp>@>|7cQ37c`#swM zMNb_vx~|y;cEm?_d!P6BgOF*5_OKsp&XFC@8`+MO`Md95*W59>tdIrXRIO)*esPpu zCJYp(@*m(md(6Sx5jbY)UNZ!d`bqZKiw=3=@CiJsv|%)A?_~H_0(&*x+yg8$6@+W+ ze42mj<7t@%O2mvtL-I(igMKT{2pUOf`C!ZaIQh{e>dD8$3sIp>Q<7J6vdTPPrCQP%Iwj6ShD#3dV4G9f?FpN&^gpfqu ztd%+P6qLjoKQ=OKA+;<}@xmGup1qi)%54Z;S;tHgY)`a~kkASCDbb{CA#rgLHGDoT z&e5heA?Fm`963VZr{Dj*-~MNR{BQr4e<7DxfUWG0=n zea}H|9;a~oWY!=KSR+I+ zFNRv!sJ>M7!rI4iV`2`$J32uSDN_DS(`+}5mJY2}F7e zNuEj`tweY<3GWBJ6MLr}ya4;^aA3#A$Ft$bHuxqsP?^h4|G6D;gm-#^HnTO9bnG-o z?`QnKQcLf)c`SJxkEW`N^84%k_5ON){hDLKH&ek^_m@Hcx<{3{{)<0t@0H>GPezTU zcF%bBYHpy_=pPjdvgSJmjqR0zYE*#ON6iOsGZZ?o1HjLwTGfbj@)(1(Es-c>@n1$6 z29Kw-r&C_&kdnQuP}kr=IYZTvO6EIxTB@~qUeYzmlhS)&wo8TOcHQJi5`iUZJv>sS z{$qit50!v&2@c{Mc!H@VrM|kbu>iAMhk+-VgB~gJT`54Q?E*f^=XFPq7b#7B-}jca zCJUWi%qB&G6T~%PB!Gu%HbLMcjp6074$haW(WNc<9tA{QUtT``@CQ`*o19>{Qo-Ny zKzDxX^al}e7U;Yt1?_fS1^hAf7_D(BW}{TLQl-K}(kb%|LZ+6L_C{ZRjOOHN>T-n_ zd1p45MWPsJs|Y9a0(f*BA|=iyC|yd>1hJ)Hzx1*qb=_EjM&OLnYgX`ES&{z{TR91O z&3j6_4|7IdVqkqBr+n`_)Q0OW=$^J8^xzN6A}8w0&ptzeY5VEvtcD&^UI>^*dF4ay zB?-*xn}>UTv-8qG*it(Jd%3NqtxYotcECqDlfa7mP0r6eM^Lml)NY9ot|U1d%gH1E zo%sNs#G`J~!_$W9^nLXiEWvd(3_-i$_r!Te@#9zH{0vtAp$C#4jY`rF3ygj!P; z+?GCmd?@mT*Mwevz!YmO9l?d5w1R(Jn8K0@pkv$ewqv01_~D~mo^@Mq3BXsb#YL)Y zSc#xbqW6?eNGTc`jL_Ea!+fi&Y@~mwAnHx}g!0K$AH+_M)fjrt`G~A&+weAN5EDF_ zZ8~KYc=`k#B-GxA6FjMSC|}DFD>2$d>e_eM*?_Ib-iu{ZO1Wfx*d8B~%GW;`U%?HQ zLN~8%uD83_y@E8u<%N=@MGAIx-0V%?VZUt99{0Z8o`LnIlRtro4&odQ{ZHo;;z)h+Dqp%Jg&5DvrGaba=n=42mj=c{xHvf`hR{G&cSRq?RFrk9bu#7 zhI5I@IV3R0<|ccSoVIhE}7Lo@9ju1A1H3J7_L|A}@${-5bq#4hVoTE$|nJfI- zR&Sk7_nX3Qpi!tvQLQy|XE0{(Q$XO*1cM^8Q}Bd@G|sROEqp7bcG^A|uH2p}Djh<=!AzH%2yn%(s z=6JwD?vlnG@jrLo=dsfsV}n;z*oIQcv;K5PpN%`d8Pw3Xzx91`8b#W&A6N7*l~|%j z1Kn6h!Y>|_pd3MC&gTOHgD3F*((G((BgIR<%evReMRpq3UKZFdv11 z^>4}K%*ca}sth2C4%Eg@B_73iy0f{@Qkc&{3u72$j@brVIX<}n6B}&@DuJvZ(wr={ zgO~+!s;CZiAZLNjG*injP^KbLqFc}%bDzN}LdLIpWx)2+Gwrzw^4MdCdIKjXPH-zN zEv?(!CN{z$^l0!%WM?zmNP?>rotqS6)8NJw`m|#@7hq2ZhMDv0ZL>iNEu?F}RYlEI z;p_;Hw_CZKA3l_{Ap|n#w5`kefq8{;@)}Il&ip_j;Or;|#aM z3NJxZw}(CLm^>IkgOiW2NUQ=fG50LD>sJq4oQpRw0pd*O_I#Z{6AQ4jfFbE_zykFv zXj^VFUI4C?ZarlwXxHn$d}J9=1&QsaN2)q+mv}y}dn>n9Q26|`EVJL%9j5MUojBUX z5;NqZ7Dyst?#MXr{dSuI{5)h?r){5Nd>HD1y_PQZ4)&6P(o)&bpD(+IjlK9zpik$xAJt6PlA+?-w+Ae=paI(CV78y6Kl>oFejve@~hyk+6~d=!99PuIIo3%W27jP z(d&_D&&Zl2=tLHJr)O$Uj2Xq$JoQaLCMMAMgn)4o;#^HUo#Qg)R2UlAQt79sk7>Q- zuUAk)X1O;*+E*8rRM&QWvAloa_HsJQD%ihn@tdb03o4o_i~so+zImK~_z}B>aUXv8 zO}<_qzkT`Qp3(9vUX5>=cj1(;%aC=?nNr`b+e`EU(5Gp-UEA;f)BpO9&0onM=k3{O z6M?f{fa%IX8H#w)hRb2TgjHE&yFu)2$uVe7LWTcHgJBRLcFJ;b<(Es*1?@E16RgpR zK3fxUam`yc)BIG+YH=?{Rn0n1j_Ke@_u>fI=Z;=Zg)tghNT5&Cyoh$cD!&6WGweCT z{ED}F)%%nYk(HiaApc^b4uUvRbOfz>bybB>5kp7#mU;NJ6=)1A04_U_;LF>{Zf|1j zAoRu<9h0JuASU03&o{Uv@~(0zVpqBbdR}UeBWpC1DCD?Uk;+Nd5b4rjw4-N=xhj%b z$sjd}D^Z92^0#nCg_RfjBEw>iQh`7oI(w=bkZ~9n7&Wn_t??KG9-UeSf{bep7CfU$|+^ z+cvrxj8XeaI6I2K{w9>d9d(>mX6T2b)*_><*;f!~UgLf4LhYk@-!-BT6Dtjju5LJ< z!CnN?d}OdYP9AnzBb>m^G8yGT%N?S&Y<8k6&sZlg@a&FeAGm=A8NTu$dAY7ICxDP0 zO;U(Sa`gl-4MQ}#!oiJ(`isUO4D@#?d604tG%YIMH{qiZh-WIfUjbEMo8k-|nv(y4X0PRbXL-2%`0;nkTLdH72vh#Arq#$0FXw)XO;KZ(N%aDl}FpJzQh02z4 zm|(M^(n<`|7&b`ZNLk`(&q)BJT-hDI0c=HQ!sTVo0G~yk=V*0f3@9jQUbnpo@SA}y zF#7^9gfj1X)qrUO?CssQr=&g}m=^+H721!W(J^VmK~|05w%nm=D=o|~>$c%^ z0}ysKgc+HH!~8%HKIeO_-JRNxeyP3t0qY+f6GN@PwCw4mX{txd@JKWxoBr4=4ieYl zI;bS@dq-A&Y{DcQ1{0Os0XN8-!vIFbfyIp8yB=cUrT`+Nwb{OnxBr?o>I$7j8$UjE>cugcyF#Fs^IJ<;c!-V+|*^$fBhraJsmaWE$2wvPb z{4lc_tMVUZM|HDQ`z0k10#LYv!XRKw5Y2>r26^yKb3^wWYChK!9JmP5l7eEUK=JTn2X>Q%}1dit;|_Uq4n?y@`oIPTY%bX#RdliO>$I`=fox?YxL zwe9iK}j)K1qs&w@(+G|>if^W z{q{u}F`~VqY_O@qv5R^bDO`W;T8|J$hwW2LK z@(Z9htX3a@2+6<_UVJE^Tt#VM2(>d7|D=?X0Gqo2| z*I$}|qC>=7+K9{`0((oBF-HVS&N_Mmfdpbljy5s}(U{?I8Txj=;wh5tfK(c1Y3k75 zMavcq6^`;LDKH+mX(I?OL`W9Vxh3O^8jN?))1Wq)s#TT<@-B%w@lNSsE4>{}BaB|V z-N=EaYUAJycuRJwe7ro72M#MtO$-|H0jFum!*L94gvKi#Yr`S*81o(G$8XGG`kgCo z)%CRB$>bzRDEU))@uS+|pWCCO_NrxmbsAMg^x>|6AAJXe@2~gQ`|H;q2mUfG;umP3 ze-maL%ELP{C7M_BtKrR(<^q^9%)+Y~Ddg?nmnrQ995N}8Bf~Ar^Y7jRbM<_KaEdgrf zLs2T|oXXHq@Y!9wx-s%8i${4#ML-r}uZ|knY?af+DmW0`T=^o8P_Tok33QP_(1O_> zYD_dUsR%;0K?KKEV4bhIsk$EzM-<<77xV_NJK6?rDuaYo$TTki!?_*Cimv*YbD3J( zw=GN&rOlp;>+`%wL6g>9{@qT%*n(>WKmknSfenz7b|~-UfY($68B0;xIoA-Zw^LIm zbu$jH)$~%N=ma$h(kTopYlSbSxzzLHB4Ah!AAYR`@!j%4&{i#TN8nc5)*ad&&X(3a zJ(>?BrR5obzNWOVaO|8>TTD_{)g3=CHC4cDQO<$@Yq^eqscyu`(N|PTtcddme&+T1 zY|MlN#L|{?lrx<-;ItN7+4}AC*Z8yuqB}oMQo9KbkRQ9=t_TSv@+k7tV9Is088E^P zsMHG7ssbS8>TFW-dV`69gZ+<#1Efg+u^D}Kv<@~oj0NXP7HWa*Csr+#I2#>Sqm2S4 zF`h1%AZ7ah(G3&Ets8)5SMM5GtP@t373f>>_xq=($Gt#vDIgVKX_}u?zHNzV3Z)I} zU;Ad3&npaB6jvC%3ER;4zTS^-M}3Dm=y7Y)or%J@@9hyoH+<|5?)VoS&6ih@IZXc= z@Q(vWvbH;N(^5`z2=(U~Rq)2tT{B9ZeGR@jHXf_^iAp+>&<^sc8KcqHM*bkUYA6}$ zaTc2c945Ox$9Wb6Gc6sn<>EoUlkc-5TgAXy(|8PV5Jy@OeL|`m^1Cq!A%B$hA^agL zd7&{()Rm?Lw1sFGdBc5|i*548XihZBz!I?RNx?G|J79+A6X$dwI79MlfDL_&6I6LF zBId-;VS0E9^D>1coF>Hb%@amYbymtB*m0ArT9*rlT&Jlihzh`+9I3w{#6Lh8JnSMJ!F(wIa)1zfTjlNsKT;5(TMOt6f~5cafBT>Q{9peGrOTe| z+Cp_C0zw_FpU?g2?lcpxOb}}+K_WIm=2oGboU%?`W&}MM6$VC9B7pB z8%vniP419NAG?$WvcB@o$8K|&=&3!z8>o2?Wqk*P?-RoJ*Zb?&{O%YM9Q#W+@fWfQ z)i1uPL&cj??LO<%!cN~#?e28|pv=*`C*g>pcf+yS>WJW;Evru8W`s?TuqBwlun~x& zS{_S*A>9MbN?8L2jHi}BA5&6Xe%+)RVk|g%p+JF=25+cRIetXc0a2$0m!XU4$wcL| za*~Jg1Z*jIFffV?Ca7MX`y8N2U1mAZbD&(>fP_BeMm3wRY5wqV`taf9=bt$U@lti} zP}&7p0806B%P=^VvLoYPA0{}3>S(lrpq|73>!jAXT*To$4kvuFPBS{ za&TEt$_NIDuI&}YYN<-FlYg4xZ0O_-#%bB8ij&8!Teme-PkTR{!kR#5gTUw%7*n6- zhw~O(O9DV)b+OgeX^?5Y~Y@xg*KS?1x31XCs z_85XTdy^~)0cnuNSb1l|#P-|uWm!&Dmu(Ajky1*hr$?#2*=>t4D1PwlamGZWoF_u9}+-=Ra2_*GY_%xM53i@*qQx}x|4B@~<46s9xBOzaL zP6aNh@+)xmxUK=nzn{(zNG^lbN7rynS?3QQPv3ocf#ycAqis`|fc@e48d$W!iv$jt z-rfqvkWz^XKFF9|yERqiblM{ULX8}>sWMD}rI6(`ufNKEM?k~t0L|R#Sp9y#nm4|} z_F$MARj&27dj0aq%cw0Ytm6ci!UIcD?41Z)1&2a98FpH}i#V_q} zFb2U`+IozUHMj&*na1psz66Ia+~SGc8TMT=f%lVAC8=%RHS=Zt?SJit%Ck@H9O>BGGL+cblYG2TL4tM~>8& zvzgElAsHcX^65_Q0lt&gXLL7g$)G)JsZ7U5494(})#P0InroIt)TiZSNEsI_C|}mi z%Q6SI<|3>5yDtq5m`bqM*DpJ641pTU>GqS)(~oIifZ*R@`+fDJJ5iEfVRWs_BX z`t$*wSGdvV#~*z9^x@0H`FXj?tyb6~#2I6FIj-Vfvf5m2+kU-nkDty4EXUSTG5*$m zUVi8IfBd80{m<<6vomnj5^!fy@zikR7LxmT4ltE@wht4s=qtwe&FJV4Z5ycmGW}Ww zM`NHH?AgzkuK-7mEfD%4IlUbQ%NiLu>Pb>(&s7*HK}5#}eEHI~Et5TD;LLhugc5@^ z)pZ7unW}h(xvDRV&GOBdo7(hgafC!T#>Hdf#3?UoS4-&RXvyt8dqH*ZcI0}3!U^tZ zE{2UJFhu02_Eyv)-1R`aPkaQJmN=stW{*KW;#4XI%M+TQEwx4sW8X1b)ifDsCnAM{ z6Yi|O!n=&=_8gpRTPJJEUmKQmP^H^35k)c(rt|7Or0@@Fk%MBS_wn#I&^*DeF&6g* zRagGGQbvcD6$k&`+-dXnwR6<(K3H7!V>;OHOoTVunWIt84XwW|o8AH8ugB|ML-_uB zfBh}@$Lp5(DkSXRD+=%IFl%3TQ7y^r5itK|vov>`hoRC@TQJj)crrza?hR%1?Qc`o zF$NcK8v~z^4-HD|AnyY#1ih0zc`Dx#Amyb%0Hp+GGcILhJU9>qfneqkvKFH14kH)? zK#UHkcvdsr5MF;$(@CvRrKVcZC`FwowRd1`jW~Eb0tHUVZ$qpBc!lfiijXyY=WPmrb5a+QQcAkMw!nzl9V&$lW1ENEu;QUXH5 z5u|(ybm_11c-+@*Hp_FqPV$I9MU=S>Y3X=`&(SkN3c+PGkB~ZG+qdXoB}%GT$HYx) zpn2ZFf-0_W-atbILZq?te7>wNW=|k2#&9~#rEG!bF;fhgsU1=zATxck)LCg9vro=k z_7o<>xWg);c3`tJhL+p6U*^ZNoVbMZ83l-XKg)S4@V&THThAYmZ;^JvmI(i=sE%`9 zE7BGwIR+1wRPD$tFdJr=@+O;hG9a_<*#pk>6|oSo>i{L# zM+8Rin?Sn^>ps-pDV0qwqM%miYlLG*kRL;&AnN_(U}t|8IGE~f%X9D;bOUw>RzTnw zj0y$n7Lzc0z)bOmJZz_7a0ov{5JJKMbk=;Pf4$x?Vv-s+z0S{cCZhLndqbu=+!OjM zNA!(?@C^~%+?|GvCbe}{?%lOHde6VXlG?*zpn{H=`fEG(t`_dhtHuppkBl0OR@lNf znw4m+v2O*&_AW~v7N&;{pc(SNH(7_rp&X=k+YU~bhQf0;s7V!^CjjqhHk#nUDUu;9 zW$X1SOQkO-N46;RQV1-iVWOpPM`O4=92Y4pHjbe*Amnk=!;Ydx9kNlSBsY5F_hYY2 zwvp=VgtW;DH>betObavKTwH;#rTl)8Ls=2v8G_shDa$^CJUN(QV&2oieYNuxCpqrf zFQ>RHX90)vr?dQ@J3Yt}p9DBQoa9~;IGyW8b(mp10Cb7w9O1%+_goq`w;e^jfW4T3 z3DNw3n=_0r4Sb>Cb?N=H4 z>v6lS1}bdXcAl>La$e+=Ai%|zvTu*S^Yq{RkN@qT{qz4LZE24396~FHy6!gkfQ%ra zDH*(-^JSeZZDyR@esXdI9%J5!a32c7IsYq=Sr!l~sR@(|C5r*JmL)?k=+(6rKwiyZ zAquDDZ3F~Xx^;21BeUqn4Vo&%DjgLx3x9D~=7stykrL?I(`0RyxK;#muLCc><& zQXr^~>=P+*YI_E$0}Zc{GK}}o=yLStZ5iFe)-?smXfrmW$%@|URe+7PGk1?nAa)qL z}R7<;)^b2<2yzatdrsJV^tXXAlbZIHz8t2dkMz?CC*JZ!H`fb2B~Q0Ii?jXrQ5nmU3fZ8 zt3b*K%XEQ+mv#+)5-cX?7IyPUFYb_8?M8sz$xJD4#>efpp@lE_hdM(S-M*HG^QksD z?FoFft*kdW@kqEL{t!eYfENPLvQ-ZXu3*G^3SpX-J?Hd%m9k-mvdiwbO-}!1z1=oU z<7D+rQiHfyxV}|Cj4_Ep^Ww ztYKIdLH+gh>y^P-)PYtE5=tM5f#jIUJ2NQ6!o@=%L4f5@PRzEGVpB?ZfgNQ?`i?}1 zBWiE7XY3scy;7du!A=&h@(mc~aOcC`ZROh_9+Z)WGqxE2J!1Rdgf-_^_SP zaS<{!^iJxYN-Q9ySH#5e{UAvNF&a4xXdO9{?kt_F;N=fWThA!R@h1!efE`u9?8Zg| z6U&+Vi=CV+2;sCU4DL<1<`>GE;s)5yNhf#^5Od`C$fSJE&Hck z3&UFEq%1j3*L@1!KQ3v#Vi08F^y%>jKl%WPUCZkg@qK;)VEp)dzx~61`&WPY=ieS< z8co(5`Id$(Y$lt_0SOTH?};peGxO8p!SQT$Gj0i&&$q`<-~8wQk{}dC%K!f}k@!xMFNsiC#$$Ka zJNKNbw!P#R1dUf>5qQRSySnP0d(We`F;c{8pPahx@+M%3<#7=dOt>sG?>&NEk&Mt& zmt+rQYO8GyXChp8n!xXhGtucF#|Cyl96E&<3zlPyD?BO(GB0Hz^;l8U;{pd0+yRI6 zbmGNX_V03@6hecL7ww=N7=@~8Rp)bMWfY@+!jrxwK^=H$- z=Rg{D&(z{>Q#-%)Gj`~&K%r5eIUwlvt|5GXyg%L_zl8~558-*peIC=k2OUd4dyo#p zhJ!=a4Xp1QWqszH4s{^8B9?_*Il!mAhaDc$iarN0_oA@&0Pr4^)a+;l!rieygT~uT z<1#}Kg$Z6x>;mHyu$@9e>zCMu!ibSU?6C8F#MV=Md)ih(c>%##^^6fmgH!j2Ygl>| zX#bH(oL~n-MomfUgh~t*Nyf>DKEFr81i3gZZ*foXjrj2KB^w)Wn^Xo7Nz3JfR3Wuq z<-DEr+?0plL+O-Y_`V8&g_s^?n8%Caomrn*EbaG=w92n0eyaiyqD1v|GXsmi)nV!{M~bUufBU^f2s z76CAfgSRCb_O(}seLhm|oEKQ1NR^!ao;>;Gp2#gUfw3x?0RyG^QnfPUxCN16(B1&?CFcuoaM$ief!-vFF*Wf-wMs7 z)V&$dwSGu9hwwNrQRh9}E;r7lz<~96nZKOm_Wk(%pL6(>|NZxq(ZH-`tRJ)_fyG*s zSN-r&&1-?~+QLj%tJ8|`cPl6*pqDqzAa_GAY#o4(K^~8x$tYt-s_pKbfo4*ze3;HD z?v?rWvVU57WD>ZsVc87DirG$jfnzj|HA7}HW7KQ+gQ1ibk_*|uvK9<2t5eRc+O16~ zjT10UXW0Z&Inf-I$edbJQ&%|EqaGayFmXaJzrf>swX{*@fr>>{O4l*!E-2_NRBn1; zH|Or=(gGT6txE(k5VakQF~lAX$%aNqs$Ess3kr`nSi8w^Pa2F211z1UiSMvR^mLjz zq$8M&GQ0|&M;ZsCXBug)RUgB`?sTrHFRsHM#+G&;03Phpj))t*aP&ct_-uz91g7p| zr}ZJpe-1f(QnoxZDDJ$|cR=|5cz?V<{=Xmfs0KNyd-m>Y&uj@(9}TiP^2-ih7Q2r< zGC@7`N9`_3I<$f4NYaj8AG5;_3u!z}N`4R1?wGKsK@PJ+H%}=M$dZa~E7-dek_tNm zOm#m%bY4LWs*DiQJ*$W;E=?wn?xdE`q&k6h1fL=m7W_!9zRS$%7|KtRD3R~MLsGqX zrUFNl6@sj&7O_T7Sq+IGPAg>Ep(QS*RGC9Ih;fAZermk%Fndl?B=8m1Y9!h6FzYJA;6k3yV`&1)@pakUv2;)9RJ! zDUg}5$Nq$xLh2`DAckyj*ZDNjtpVIH)H<{g*dTC& zp(LrxI-L^yG1Y$CneYzuBSj)Ua2sBZ@KB{@37#3wK)j=1%MZIw`7Im#IixgHpFm3! zs92m5Kz)ljyxvZy7u0((dMBVb3FKTB|MSnE6w>X{$JTDK*6x>JtJ!Y*>)Y*odO`Ly z#&(&yKB-FdzDKE!_n2>cma=;fpv7Ur5~E5!Aao27mW+PT0-*uk;OzmDEn?vf4Qgt2 z^wdd-;f(-~{Z>#TP975x!~)=_iWI&mP^9KcAwZ8rQ(8rmq2N0OWhJNoUP2P z^pm}%G};-eO%Cc=b~J|h0bvy!B74Fo#|tR1>M?dxzbbeRBB*|FotPriy6opJ9vq{< zu_c%>gVTrOXw&zsnMeEmF~1wH*q(=neXgTN(+42icFrlUn(6Zht@UC0j_9{qkF@Af z`Y{W?xtOx# z;UScg*$=qv21$i(LJvjJ+(=LyrJY7lqHGSi?vRnvGPK9oF=!l9;0``>34RhlG=&`? zsnLF8ISG6;$-sr!$@_5FqH1v-y&b#=hD_d-66pVr2@$SHDIls+>4k4!Zg`}OYuU2i zLcNvk?HaUolPUAE&ZpCIUgl+Y-n#XyXX6;oCvaRjJD+EKe^z$4S^l16>p7bec4Gt> z>$GpUc8F;O6q(k=kfnIJ?Q#dkxL;zC9T91T$cRu^xu74vYC>Ed?XO+vBNXxW41W8{d#4Z#?O!)cCUp_os4_A!8Poe2dC*)o_P(oxvM zGzo@B1p!WaV#Wssij0D{JsHLzZ3y0GboDfhScNaM_axer&J1M+H5!I;P=kg?ME9}> zAnA@=<;I%~1396!R1sK`L5`f+*+C}9Q#%f9-6sa(LWD^5Q6Rn>14N$5uht{Y0`q@R zNVVw>%4|Z9c9T9>1ZB7s+fHe~cYN^SsqG~wXT!_1gM`khrM|bZFxj-KjxFo(n(6z| zv%B%r_H=A6?||_A@&0&!{C1e5PZUwDKJR}0`pJ*Y@c6MKyzuxR`ZK>7qYn{xJ)Un> z4rlEBlr358YCBYGY}QGaYSv_>96N z1zmnAb7s_q*IP2%rS!^~r8eBs_voAl+fTzz5Fch7Z=NpYjaEYZb74!(S3^5xAYjWmJ0yq6az7;Bp9ck%^c)SGy6M=tI zThBA;IvP_oBk-)mw>PbroY5i}U(3>?4H9rKuoS%G1pY=vI#;p4T`k00#T-nEAp#WG zU!v2@%D_s+CNCa$47jG#$-u2ABRWvoJq5K2Xq4*;uf6>i<%i^#=M*Ryqj7?5Qikqt z(a8r;YWclXl>XCCult+8Wb_tthIgmDtLtr(&k~>~ikB^hRL?Ibsn$X)S-{`6!R`RD z8j5CtAdQA_E$n8OB)=$~X&HdvkV#2{fua7{)1O4eIePZX-D$}zFRqrefHrKtRJEltN z?o&#C5Yq z1p65Q3bUY*GW%(QYA$Hm1k}=k4?8k3OLp?3c6MlC1i5i`7V2oC?fWjbxnQrDB05MB zo?PHu(@+cfDw*vj*3^O;r#|v6boz&L6Q{4f5%3hRum9P%%jx46 zYK#8k=}&+8?r%T*!!`#?*zOR5D1zGU$K=M6Z$vhaw_7x`UoXL|9#&kkQX_}y0>55P z>mR=lKVDqEItQRqqDMZ2Y(^dfx%1hp8e@%9Dwj{&*S~}L4V=Opu5OHUL}3hiLnOVt za}7~oPupW)g6b+qst=$qUYCaS92It~yonv4stLk*OvtIRAfb=xulV8s-N-yNG(=WN zAu-Ua3$C>wR06+R@YO;0Fq9o4)P@F}a=l?miU6g|)O4O=m@P9u*trHkj!9zHH^GUk zDiK=_Qcycxfi|QQX5Eg_N%R43tsAd3GEu6WRND?}eGiSO-4so=CS)*S?9S=G?CV-Hm{G6GC9jFe!ignsZm4~96mA=M$@vF~wM zs75=wwUKf9
" + payload.session_name + "
" + payload.session_date, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, [{ + id: "btn-more-info", + text: "MORE INFO", + rel: "external", + "class": "button-orange", + callback: gotoLessonBookingPage, + callback_args: { + "lesson_session_id": payload.lesson_session_id + } + }] + ); + }); + } function registerScheduledSessionRsvp() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP, function(header, payload) { @@ -1337,6 +1376,10 @@ context.JK.popExternalLink('/sessions/' + args.session_id + '/details'); } + function gotoLessonBookingPage(args) { + window.location.href = "/client#/jamclass/lesson-booking/" + args.lesson_session_id + } + function deleteNotificationHandler(evt) { evt.stopPropagation(); var notificationId = $(this).attr('notification-id'); diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index aa06843e3..7d83a8e56 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -3,8 +3,12 @@ //= require_directory ./react-components/helpers //= require_directory ./react-components/actions //= require ./react-components/stores/AppStore +//= require ./react-components/stores/NavStore //= require ./react-components/stores/UserStore //= require ./react-components/stores/UserActivityStore +//= require ./react-components/stores/SchoolStore +//= require ./react-components/stores/StripeStore +//= require ./react-components/stores/AvatarStore //= require ./react-components/stores/InstrumentStore //= require ./react-components/stores/LanguageStore //= require ./react-components/stores/GenreStore diff --git a/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee new file mode 100644 index 000000000..c91bd6389 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountSchoolScreen.js.jsx.coffee @@ -0,0 +1,451 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +SchoolActions = context.SchoolActions +SchoolStore = context.SchoolStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountSchoolScreen = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(SchoolStore, "onSchoolChanged") + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + TILE_ACCOUNT: 'account' + TILE_MEMBERS: 'members' + TILE_EARNINGS: 'earnings' + TILE_AGREEMENT: 'agreement' + + TILES: ['account', 'members', 'earnings', 'agreement'] + + onAppInit: (@app) -> + @app.bindScreen('account/school', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onSchoolChanged: (schoolState) -> + @setState(schoolState) + + onUserChanged: (userState) -> + @noSchoolCheck(userState?.user) + @setState({user: userState?.user}) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}] + @root = $(@getDOMNode()) + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({userSchedulingComm: value}) + + + beforeHide: (e) -> + #ProfileActions.viewTeacherProfileDone() + @screenVisible = false + + beforeShow: (e) -> + + noSchoolCheck: (user) -> + if user?.id? && @screenVisible + + if !user.owned_school_id? + window.JK.Banner.showAlert("You are not the owner of a school in our systems. If you are, please contact support@jamkazam.com and we'll update your account.") + return false + else + if !@shownOnce + @shownOnce = true + SchoolActions.refresh(user.owned_school_id) + + return true + + else + return false + + afterShow: (e) -> + @screenVisible = true + logger.debug("AccountSchoolScreen: afterShow") + logger.debug("after show", @state.user) + @noSchoolCheck(@state.user) + + getInitialState: () -> + { + school: null, + user: null, + selected: 'account', + userSchedulingComm: null, + updateErrors: null, + schoolName: null, + studentInvitations: null, + teacherInvitations: null, + updating: false + } + + isSchoolManaged: () -> + if this.state.userSchedulingComm? + this.state.userSchedulingComm == 'school' + else + this.state.school.scheduling_communication == 'school' + + nameValue: () -> + if this.state.schoolName? + this.state.schoolName + else + this.state.school.name + + nameChanged: (e) -> + $target = $(e.target) + val = $target.val() + @setState({schoolName: val}) + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + onUpdate: (e) -> + e.preventDefault() + + if this.state.updating + return + name = @root.find('input[name="name"]').val() + if @isSchoolManaged() + scheduling_communication = 'school' + else + scheduling_communication = 'teacher' + correspondence_email = @root.find('input[name="correspondence_email"]').val() + + @setState(updating: true) + rest.updateSchool({ + id: this.state.school.id, + name: name, + scheduling_communication: scheduling_communication, + correspondence_email: correspondence_email + }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + onUpdateDone: (response) -> + @setState({school: response, userSchedulingComm: null, schoolName: null, updateErrors: null, updating: false}) + + @app.layout.notify({title: "update success", text: "Your school information has been successfully updated"}) + + onUpdateFail: (jqXHR) -> + handled = false + + @setState({updating: false}) + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + handled = true + @setState({updateErrors: errors}) + + + if !handled + @app.ajaxError(jqXHR, null, null) + + inviteTeacher: () -> + @app.layout.showDialog('invite-school-user', {d1: true}) + + inviteStudent: () -> + @app.layout.showDialog('invite-school-user', {d1: false}) + resendInvitation: (id, e) -> + e.preventDefault() + rest.deleteSchoolInvitation({ + id: + id: this.state.school.id, invitation_id: id + }).done((response) => @resendInvitationDone(response)).fail((jqXHR) => @resendInvitationFail(jqXHR)) + + resendInvitationDone: (response) -> + @app.layout.notify({title: 'invitation resent', text: 'invitation was resent to ' + response.email}) + + resendInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + deleteInvitation: (id, e) -> + e.preventDefault() + rest.deleteSchoolInvitation({ + id: + id: this.state.school.id, invitation_id: id + }).done((response) => @deleteInvitationDone(id, response)).fail((jqXHR) => @deleteInvitationFail(jqXHR)) + + deleteInvitationDone: (id, response) -> + context.SchoolActions.deleteInvitation(id) + + deleteInvitationFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + + removeFromSchool: (id, isTeacher, e) -> + if isTeacher + rest.deleteSchoolTeacher({id: this.state.school.id, teacher_id: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + else + rest.deleteSchoolStudent({id: this.state.school.id, student: id}).done((response) => @removeFromSchoolDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR)) + + removeFromSchoolDone: (school) -> + context.JK.Banner.showNotice("User removed", "User was removed from your school.") + context.SchoolActions.updateSchool(school) + + removeFromSchoolFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + renderUser: (user, isTeacher) -> + photo_url = user.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + `
+
+ +
+
+ {user.name} +
+ +
` + + + renderInvitation: (invitation) -> + `
+ + + + + +
{invitation.first_name} {invitation.last_name} +
has not yet accepted invitation
+ resend invitation + delete +
+ +
+
` + + renderTeachers: () -> + teachers = [] + + if this.state.school.teachers? && this.state.school.teachers.length > 0 + for teacher in this.state.school.teachers + teachers.push(@renderUser(teacher.user, true)) + else + teachers = `

No teachers

` + + teachers + + renderStudents: () -> + students = [] + + if this.state.school.students? && this.state.school.students.length > 0 + for student in this.state.school.students + students.push(@renderUser(student, false)) + else + students = `

No students

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

No pending invitations

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

No pending invitations

` + invitations + + mainContent: () -> + if !@state.user? || !@state.school? + `
Loading...
` + else if @state.selected == @TILE_ACCOUNT + @account() + else if @state.selected == @TILE_MEMBERS + @members() + else if @state.selected == @TILE_EARNINGS + @earnings() + else if @state.selected == @TILE_AGREEMENT + @agreement() + else + @account() + + account: () -> + ownerEmail = this.state.school.owner.email + correspondenceEmail = this.state.school.correspondence_email + correspondenceDisabled = !@isSchoolManaged() + + nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) + correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors) + nameClasses = classNames({name: true, error: nameErrors?, field: true}) + correspondenceEmailClasses = classNames({ + correspondence_email: true, + error: correspondenceEmailErrors?, + field: true + }) + + cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating } + updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating } + + `
+
+ + + {nameErrors} +
+
+ + +
+ +

Management Preference

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

Payments

+ +
+ +
+ +
+ CANCEL + UPDATE +
+
` + + + members: () -> + teachers = @renderTeachers() + teacherInvitations = @renderTeacherInvitations() + + students = @renderStudents() + studentInvitations = @renderStudentInvitations() + + `
+
+
+

teachers:

+ INVITE TEACHER +
+
+
+ {teacherInvitations} +
+ +
+ {teachers} +
+
+ +
+
+

students:

+ INVITE STUDENT +
+
+
+ {studentInvitations} +
+
+ {students} +
+
+ +
` + + earnings: () -> + `
+

Coming soon

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

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

+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @setState({selected: selection}) + + createTileLink: (i, tile) -> + active = this.state.selected == tile + classes = classNames({last: i == @TILES.length - 1, activeTile: active}) + + return `` + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @TILES + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNav = `
+ {profileSelections} +
` + + `
+
+
school:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee new file mode 100644 index 000000000..4b08de848 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AvatarEditLink.js.jsx.coffee @@ -0,0 +1,55 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AvatarStore = context.AvatarStore + +@AvatarEditLink = React.createClass({ + + mixins: [ + Reflux.listenTo(AvatarStore, "onAvatarUpdate"), + ] + + onAvatarUpdate: (avatar) -> + @setState({avatar: avatar}) + + getInitialState: () -> + { + avatar: null, + imageLoadedFpfile: null + } + + componentWillMount: () -> + + + componentDidMount: () -> + @root = $(@getDOMNode()) + + startUpdate: () -> + AvatarActions.start(this.props.target, this.props.target_type) + + render: () -> + if this.props.target?.photo_url? + + testStudentUrl = "/school/#{this.props.target.id}/student?preview=true" + testTeacherUrl = "/school/#{this.props.target.id}/teacher?preview=true" + + `
+ +
+ change/update logo
+ +
See how it will look to  + students and  + teachers +
+
` + else + `
+
+ No logo graphic uploaded +
+ change/update logo +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee b/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee new file mode 100644 index 000000000..c6e1203f6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AvatarUploader.js.jsx.coffee @@ -0,0 +1,168 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AvatarStore = context.AvatarStore + +@AvatarUploader = React.createClass({ + + mixins: [ + Reflux.listenTo(@AppStore, "onAppInit"), + Reflux.listenTo(AvatarStore, "onAvatarUpdate"), + ] + + onAppInit: (@app) -> + + onAvatarUpdate: (avatar) -> + @setState({avatar: avatar}) + + getInitialState: () -> + { + avatar: null, + imageLoadedFpfile: null + } + + componentWillMount: () -> + + + componentDidMount: () -> + @root = $(@getDOMNode()) + + # get filepicker loaded! + `(function (a) { + if (window.filepicker) { + return + } + var b = a.createElement("script"); + b.type = "text/javascript"; + b.async = !0; + b.src = ("https:" === a.location.protocol ? "https:" : "http:") + "//api.filepicker.io/v1/filepicker.js"; + var c = a.getElementsByTagName("script")[0]; + c.parentNode.insertBefore(b, c); + var d = {}; + d._queue = []; + var e = "pick,pickMultiple,pickAndStore,read,write,writeUrl,export,convert,store,storeUrl,remove,stat,setKey,constructWidget,makeDropPane".split(","); + var f = function (a, b) { + return function () { + b.push([a, arguments]) + } + }; + for (var g = 0; g < e.length; g++) { + d[e[g]] = f(e[g], d._queue) + } + window.filepicker = d + })(document);` + + componentDidUpdate: () -> + #$jcropHolder = @root.find('.jcrop-holder') + #$spinner = @root.find('.spinner-large') + # $spinner.width($jcropHolder.width()); + # $spinner.height($jcropHolder.height()); + + startUpdate: (e) -> + e.preventDefault(); + + AvatarActions.pick() + + onSelect: (selection) -> + AvatarActions.select(selection) + + imageLoaded: (e) -> + avatar = $(e.target) + width = avatar.naturalWidth(); + height = avatar.naturalHeight(); + logger.debug("image loaded", avatar, width, height) + + storedSelection = this.state.avatar?.currentCropSelection + if storedSelection? + left = storedSelection.x + right = storedSelection.x2 + top = storedSelection.y + bottom = storedSelection.y2 + else + if width < height + left = width * .25 + right = width * .75 + top = (height / 2) - (width / 4) + bottom = (height / 2) + (width / 4) + else + top = height * .25 + bottom = height * .75 + left = (width / 2) - (height / 4) + right = (width / 2) + (height / 4) + + container = @root.find('.avatar-space') + + jcrop = avatar.data('Jcrop'); + + if jcrop + logger.debug("jcrop destroy!") + jcrop.destroy() + + @root.find('.jcrop-holder').remove() + + logger.debug("initial selection box: left: #{left}, top: #{top}, right: #{right}, bottom: #{bottom}") + avatar.Jcrop({ + aspectRatio: 1, + boxWidth: container.width() * 1, + boxHeight: container.height() * 1, + setSelect: [left, top, right, bottom], + trueSize: [width, height], + onSelect: this.onSelect + }); + + @setState({imageLoadedFpfile: this.state.avatar.currentFpfile}) + + imageLoadError: () -> + logger.debug("image did not load") + + + doCancel: (e) -> + e.preventDefault() + @app.layout.closeDialog('upload-avatar', true); + + doDelete: (e) -> + e.preventDefault() + AvatarActions.delete() + + doSave: (e) -> + AvatarActions.update() + + render: () -> + if this.state.avatar?.signedCurrentFpfile? + fpfile = this.state.avatar.currentFpfile + signedUrl = this.state.avatar.signedCurrentFpfile + #console.log("filefile info", fpfile, signedUrl, this.state.imageLoadedFpfile) + #spinner = `
` + if fpfile.url != this.state.imageLoadedFpfile?.url || this.state.avatar.updatingAvatar || this.state.avatar.pickerOpen + spinner = `
` + + `
+
+ +
+ {spinner} + +
` + + else + `
+
+
+ No logo graphic uploaded +
+ +
+ + +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee new file mode 100644 index 000000000..72edef0cd --- /dev/null +++ b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee @@ -0,0 +1,524 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@BookLesson = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + onAppInit: (@app) -> + @app.bindScreen('jamclass/book-lesson', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + @setState({user: userState?.user}) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({ recurring: value }) + + componentDidMount: () -> + @checkboxes = [{selector: 'input.lesson-frequency', stateKey: 'lesson-frequency'}] + @root = $(@getDOMNode()) + + @slot1Date = @root.find('.slot-1 .date-picker') + @slot2Date = @root.find('.slot-2 .date-picker') + @slot1Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @slot2Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @iCheckify() + + componentDidUpdate:() -> + @iCheckify() + @slot1Date = @root.find('.slot-1 .date-picker') + @slot2Date = @root.find('.slot-2 .date-picker') + @slot1Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + @slot2Date.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e) => @toggleDate(e)) + }) + + + toggleDate: (e) -> + + isNormal: () -> + @state.type == 'normal' + + isTestDrive: () -> + @state.type == 'test-drive' + + parseId:(id) -> + if !id? + {id: null, type: null} + else + bits = id.split('_') + if bits.length == 2 + {id: bits[1], type: bits[0]} + else + {id: null, type: null} + + beforeHide: (e) -> + logger.debug("BookLesson: beforeHide") + @resetErrors() + + beforeShow: (e) -> + logger.debug("BookLesson: beforeShow", e.id) + + afterShow: (e) -> + logger.debug("BookLesson: afterShow", e.id) + + parsed = @parseId(e.id) + + id = parsed.id + + @setState({teacherId: id, type: parsed.type}) + @resetErrors() + rest.getUserDetail({ + id: id, + show_teacher: true + }).done((response) => @userDetailDone(response)).fail(@app.ajaxError) + + userDetailDone: (response) -> + if response.id == @state.teacherId + @setState({teacher: response, isSelf: response.id == context.JK.currentUserId}) + else + logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId) + + getInitialState: () -> + { + user: null, + teacher: null, + teacherId: null, + generalErrors: null, + descriptionErrors: null, + bookedPriceErrors: null, + slot1Errors: null, + slot2Errors: null + updating: false, + recurring: 'single' + } + + jamclassPolicies: (e) -> + e.preventDefault() + context.JK.popExternalLink($(e.target).attr('href')) + + getSlotData: (position) -> + $slot = @root.find('.slot-' + (position + 1)) + picker = $slot.find('.date-picker') + + hour = $slot.find('.hour').val() + minute = $slot.find('.minute').val() + am_pm = $slot.find('.am_pm').val() + + + if hour? and hour != '' + hour = new Number(hour) + if am_pm == 'PM' + hour += 12 + else + hour = null + + if minute? and minute != '' + minute = new Number(minute) + else + minute = null + + if !@isRecurring() + date = picker.datepicker("getDate") + if date? + date = context.JK.formatDateYYYYMMDD(date) + else + day_of_week = $slot.find('.day_of_week').val() + + + {hour: hour, minute: minute, date: date, day_of_week: day_of_week} + + resetErrors: () -> + @setState({generalErrors: null, slot1Errors: null, slot2Errors: null, descriptionErrors: null, bookedPriceErrors: null}) + + isRecurring: () -> + @state.recurring == 'recurring' + + isMonthly: () -> + if !@isRecurring() + return false + + parsed = @bookingOption() + return parsed? && parsed.frequency == 'monthly' + + bookingOption: () -> + select = @root.find('.booking-options-for-teacher') + value = select.val() + @parseBookingOption(value) + + # select format = frequency|lesson_length , where frequency is 'monthly' or 'weekly' + parseBookingOption: (value) -> + if !value? + return null + bits = value.split('|') + if !bits? || bits.length != 2 + return null + return {frequency: bits[0], lesson_length: bits[1]} + + + onBookLesson: (e) -> + e.preventDefault() + + if $(e.target).is('.disabled') + return + + options = {} + options.teacher = this.state.teacher.id + + options.slots = [@getSlotData(0), @getSlotData(1)] + options.timezone = window.jstz.determine().name() + description = @root.find('textarea.user-description').val() + if description == '' + description == null + options.description = description + + if @isTestDrive() + options.payment_style = 'elsewhere' + options.lesson_type = 'test-drive' + + else if @isNormal() + options.lesson_type = 'paid' + if @isRecurring() + if @isMonthly() + options.payment_style = 'monthly' + else + options.payment_style = 'weekly' + else + options.payment_style = 'single' + + options.recurring = @isRecurring() + parsed = @bookingOption() + if parsed? + options.lesson_length = parsed.lesson_length + + + else + throw "Unable to determine lesson type" + + @resetErrors() + @setState({updating: true}) + rest.bookLesson(options).done((response) => @booked(response)).fail((jqXHR) => @failedBooking(jqXHR)) + + booked: (response) -> + @setState({updating: false}) + if response.user['has_stored_credit_card?'] + context.location ="/client#/jamclass/lesson-session/" + response.id + else + context.location = '/client#/jamclass/payment' + + failedBooking: (jqXHR) -> + @setState({updating: false}) + if jqXHR.status == 422 + body = JSON.parse(jqXHR.responseText) + + generalErrors = {errors: {}} + for errorType, errors of body.errors + if errorType == 'description' + @setState({descriptionErrors: errors}) + else if errorType == 'booked_price' + @setState({bookedPriceErrors: errors}) + else if errorType == 'lesson_length' + # swallow, because 'booked_price' covers this + else if errorType == 'lesson_booking_slots' + # do nothing. these are handled better by the _children errors + else + generalErrors.errors[errorType] = errors + + for childErrorType, childErrors of body._children + if childErrorType == 'lesson_booking_slots' + slot1Errors = childErrors[0] + slot2Errors = childErrors[1] + if Object.keys(slot1Errors["errors"]).length > 0 + @setState({slot1Errors: slot1Errors}) + if Object.keys(slot2Errors["errors"]).length > 0 + @setState({slot2Errors: slot2Errors}) + if Object.keys(generalErrors.errors).length > 0 + @setState({generalErrors: generalErrors}) + + onCancel: (e) -> + e.preventDefault() + + isTestDrive: () -> + @state.type == 'test-drive' + + isNormal: () -> + @state.type == 'normal' + + constructBookingOptions: () -> + results = [] + if !@state.teacher? + return results + + teacher = @state.teacher.teacher + enabledMinutes = [] + for minutes in [30, 45, 60, 90, 120] + duration_enabled = teacher["lesson_duration_#{minutes}"] + + if duration_enabled + enabledMinutes.push(minutes) + + if !@isRecurring() + for minutes in enabledMinutes + lesson_price = teacher["price_per_lesson_#{minutes}_cents"] + value = "single|#{minutes}" + display = "#{minutes} Minute Lesson for $#{(lesson_price / 100).toFixed(2)}" + results.push(``) + 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(``) + + + 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 results.length == 0 + results.push(``) + else + results.unshift(``) + results + + render: () -> + photo_url = teacher?.photo_url + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + + teacher = @state.teacher + + if teacher? + name = `
{teacher.name}
` + teacher_first_name = teacher.first_name + else + name = `
Loading...
` + teacher_first_name = '...' + + hours = [] + for hour in ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] + if hour == '12' + key = '00' + else + key = hour + hours.push(``) + + minutes = [] + for minute in ['00', '15', '30', '45'] + minutes.push(``) + + am_pm = [``, ``] + + bookLessonClasses = classNames({"button-orange": 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) + slot1Errors = context.JK.reactErrors(@state.slot1Errors, {preferred_day: 'Date', day_of_week: 'Day'}) + slot2Errors = context.JK.reactErrors(@state.slot2Errors, {preferred_day: 'Date', day_of_week: 'Day'}) + generalErrors = context.JK.reactErrors(@state.generalErrors, {user: 'You'}) + + bookedPriceClasses = classNames({booked_price: true, error: bookedPriceErrors?, field: true, 'booking-options': true}) + descriptionClasses = classNames({description: true, error: descriptionErrors?}) + slot1Classes = classNames({slot: true, 'slot-1': true, error: slot1Errors?}) + slot2Classes = classNames({slot: true, 'slot-2': true, error: slot2Errors?}) + generalClasses = classNames({general: true, error: generalErrors?}) + + + if !@isRecurring() + slots = + `
+
+
What date/time do you prefer for your lesson?
+
+ + + +
+
+ + : +
+ {slot1Errors} +
+
+
What is a second date/time option if preferred not available?
+
+ + + +
+
+ + : +
+ {slot2Errors} +
+
` + else + days = [] + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + days.push(``) + + + slots = + `
+
+
What day/time do you prefer for your lesson?
+
+ + + +
+
+ + : +
+ {slot1Errors} +
+
+
What is a second day/time option if preferred not available?
+
+ + + +
+
+ + : +
+ {slot2Errors} +
+
` + + + if @isTestDrive() + header = `

book testdrive lesson

` + if @state.user?.remaining_test_drives == 1 + testDriveLessons = "1 TestDrive lesson credit" + else + testDriveLessons = "#{this.state.user?.remaining_test_drives} TestDrive lesson credits" + + actions = `` + + columnLeft = `
+ {header} + {slots} +
+
Tell {teacher_first_name} a little about yourself as a student.
+ + +
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee new file mode 100644 index 000000000..b166a9404 --- /dev/null +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -0,0 +1,482 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@LessonPayment = React.createClass({ + + mixins: [ + ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shouldShowNameSet: false + + onAppInit: (@app) -> + @app.bindScreen('jamclass/lesson-payment', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + + if !@shouldShowNameSet + @shouldShowNameSet = true + if userState?.user? + username = userState.user.name + first_name = userState.user.first_name + last_name = userState.user.last_name + shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1 + else + shouldShowName = @state.shouldShowName + + @setState({user: userState?.user, shouldShowName: shouldShowName}) + + + componentDidMount: () -> + @checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}] + + @root = $(@getDOMNode()) + @root.find('input.expiration').payment('formatCardExpiry') + @root.find("input.card-number").payment('formatCardNumber') + @root.find("input.cvv").payment('formatCardCVC') + @iCheckify() + + componentDidUpdate: () -> + @iCheckify() + + getInitialState: () -> + { + user: null, + lesson: null, + updating: false, + billingInUS: true, + userWantsUpdateCC: false + } + + beforeHide: (e) -> + @resetErrors() + + beforeShow: (e) -> + + afterShow: (e) -> + @resetState() + @resetErrors() + @setState({updating: true}) + rest.getUnprocessedLessonOrIntent().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR)) + + resetErrors: () -> + @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null}) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + @setState({billingInUS: checked}) + + resetState: () -> + @setState({updating: false, lesson: null}) + + unprocessLoaded: (response) -> + @setState({updating: false}) + logger.debug("unprocessed loaded", response) + @setState(response) + + failedBooking: (jqXHR) -> + @setState({updating: false}) + @setState({lesson: null}) + if jqXHR.status == 404 + # no unprocessed lessons. That's arguably OK; the user is just going to enter their info up front. + console.log("nothing") + + failedUnprocessLoad: (jqXHR) -> + @setState({updating: false}) + @app.layout.notify({ + title: 'Unable to load lesson', + text: 'Please attempt to book a free lesson first or refresh this page.' + }) + + onBack: (e) -> + e.preventDefault() + window.location.href = '/client#/teachers/search' + + onSubmit: (e) -> + @resetErrors() + + e.preventDefault() + + if !window.Stripe? + @app.layout.notify({ + title: 'Payment System Not Loaded', + text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!" + }) + else + if !@state.userWantsUpdateCC + @attemptPurchase(null) + else + + ccNumber = @root.find('input.card-number').val() + expiration = @root.find('input.expiration').val() + cvv = @root.find('input.cvv').val() + inUS = @root.find('input.billing-address-in-us').is(':checked') + zip = @root.find('input.zip').val() + + error = false + + + if @state.shouldShowName + name = @root.find('#set-user-on-card').val() + + if name.indexOf('Anonymous') > -1 + @setState({nameError: true}) + error = true + + if !$.payment.validateCardNumber(ccNumber) + @setState({ccError: true}) + error = true + + bits = expiration.split('/') + + if bits.length == 2 + month = bits[0].trim(); + year = bits[1].trim() + + month = new Number(month) + year = new Number(year) + + if year < 2000 + year += 2000 + + if !$.payment.validateCardExpiry(month, year) + @setState({expiryError: true}) + error = true + else + @setState({expiryError: true}) + error = true + + + cardType = $.payment.cardType(ccNumber) + + if !$.payment.validateCardCVC(cvv, cardType) + @setState({cvvError: true}) + error = true + + if inUS && (!zip? || zip == '') + @setState({zipCodeError: true}) + + if error + return + + data = { + number: ccNumber, + cvc: cvv, + exp_month: month, + exp_year: year, + } + + window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response))); + + stripeResponseHandler: (status, response) -> + console.log("response", response) + + if response.error + if response.error.code == "invalid_number" + @setState({ccError: true, cvvError: null, expiryError: null}) + else if response.error.code == "invalid_cvc" + @setState({ccError: null, cvvError: true, expiryError: null}) + else if response.error.code == "invalid_expiry_year" || response.error.code == "invalid_expiry_month" + @setState({ccError: null, cvvError: null, expiryError: true}) + else + @attemptPurchase(response.id) + + attemptPurchase: (token) -> + if this.state.billingInUS + zip = @root.find('input.zip').val() + + data = { + token: token, + zip: zip, + test_drive: @state.lesson?.lesson_type == 'test-drive' || (@state.intent?.intent == 'book-test-drive') + } + + if @state.shouldShowName + data.name = @root.find('#set-user-on-card').val() + + rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR)) + + stripeSubmitted: (response) -> + logger.debug("stripe submitted", response) + + if @state.shouldShowName + window.UserActions.refresh() + + # if the response has a lesson, take them there + if response.lesson?.id? + context.Banner.showNotice({ + title: "Lesson Requested", + text: "The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email." + }) + window.location = "/client#/jamclass/lesson-session/" + response.lesson.id + else if response.test_drive? || response.intent_book_test_drive? + + if response.test_drive?.teacher_id + teacher_id = response.test_drive?.teacher_id + else if response.intent_book_test_drive? + teacher_id = response.intent_book_test_drive?.teacher_id + + if teacher_id? + text = "You now have 4 lessons that you can take with 4 different teachers.

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

We've taken you automatically to the Teacher Search screen, so you can search for teachers right for you." + location = "/client#/teachers/search" + context.JK.Banner.showNotice({ + title: "Test Drive Purchased", + text: text + }) + window.location = location + else + window.location = "/client#/teachers/search" + + stripeSubmitFailure: (jqXHR) -> + handled = false + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + if errors.errors.name? + @setState({name: errors.errors.name[0]}) + handled = true + else if errors.errors.user? + @app.layout.notify({title: "Can't Purchase Test Drive", text: "You " + errors.errors.user[0] + '.' }) + handled = true + + if !handled + @app.notifyServerError(jqXHR, 'Credit Card Not Stored') + + onUnlockPaymentInfo: (e) -> + e.preventDefault() + @setState({userWantsUpdateCC: true}) + + onLockPaymentInfo: (e) -> + e.preventDefault() + @setState({userWantsUpdateCC: false}) + + reuseStoredCard: () -> + !@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] + + render: () -> + disabled = @state.updating || @reuseStoredCard() + + if @state.updating + photo_url = '/assets/shared/avatar_generic.png' + name = 'Loading ...' + teacherDetails = `
+
+ +
+ {name} +
` + else + if @state.lesson? || @state.intent? + if @state.lesson? + photo_url = @state.lesson.teacher.photo_url + name = @state.lesson.teacher.name + lesson_length = this.state.lesson.lesson_length + lesson_type = this.state.lesson.lesson_type + else + photo_url = @state.intent.teacher.photo_url + name = @state.intent.teacher.name + lesson_length = 30 + lesson_type = @state.intent.intent.substring('book-'.length) + + if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' + teacherDetails = `
+
+ +
+ {name} +
` + + if lesson_type == 'single-free' + header = `

enter card info

+ +
Your card wil not be charged.
See explanation to the right.
+
` + bookingInfo = `

You are booking a single free {lesson_length}-minute lesson.

` + bookingDetail = `

To book this lesson, you will need to enter your credit card information. + You will absolutely not be charged for this free lesson, and you have no further commitment to purchase + anything. We have to collect a credit card to prevent abuse by some users who would otherwise set up + multiple free accounts to get multiple free lessons. +
+ +

+

` + else if lesson_type == 'test-drive' + + + if @reuseStoredCard() + header = `

purchase test drive

` + else + header = `

enter payment info for test drive

` + + bookingInfo = `

` + bookingDetail = `

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. +
+ + jamclass + policies +

` + else if lesson_type == 'paid' + header = `

enter payment info for lesson

` + if this.state.lesson.recurring + if this.state.lesson.payment_style == 'single' + bookingInfo = `

You are booking a {lesson_length} minute lesson for + ${this.state.lesson.booked_price.toFixed(2)}

` + bookingDetail = `

+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your + lesson is scheduled, or you will be charged for the lesson in full. +
+ + jamclass + policies +

` + 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.

` + 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. +
+ + jamclass + policies +

` + 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.

` + 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 + month, or you will be charged for that month in full. +
+ + jamclass + policies +

` + else + bookingInfo = `

You are booking a {lesson_length} minute lesson for + ${this.state.lesson.booked_price.toFixed(2)}

` + bookingDetail = `

+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your + lesson is scheduled, or you will be charged for the lesson in full. +
+ + jamclass + policies +

` + else + header = `

enter payment info

` + bookingInfo = `

You are entering your credit card info so that later checkouts go quickly. You can skip this + for now.

` + bookingDetail = ` +

+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your + lesson is scheduled, or you will be charged for the lesson in full. +
+ + jamclass policies +

` + + + submitClassNames = {'button-orange': true, disabled: disabled && @state.updating} + updateCardClassNames = {'button-grey': true, disabled: disabled && @state.updating} + backClassNames = {'button-grey': true, disabled: disabled && @state.updating} + + cardNumberFieldClasses = {field: true, "card-number": true, error: @state.ccError} + expirationFieldClasses = {field: true, "expiration": true, error: @state.expiryError} + cvvFieldClasses = {field: true, "card-number": true, error: @state.cvvError} + inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError} + zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError} + nameClasses= {field: true, "name": true, error: @state.nameError} + formClasses= {stored: @reuseStoredCard()} + leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()} + rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()} + + if @state.user?['has_stored_credit_card?'] + if @state.userWantsUpdateCC + updateCardAction = `NEVERMIND, USE MY STORED PAYMENT INFO` + leftPurchaseActions = `
+ BACK + {updateCardAction} + PURCHASE +
` + else + updateCardAction = `I'D LIKE TO UPDATE MY PAYMENT INFO` + rightPurchaseActions = `
+ BACK + {updateCardAction} + PURCHASE +
` + else + leftPurchaseActions = `` + if @state.shouldShowName && @state.user?.name? + username = @state.user?.name + nameField = + `
+ + +
` + + `
+
+ {header} + +
+ {nameField} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ {leftPurchaseActions} +
+
+ {teacherDetails} +
+ {bookingInfo} + {bookingDetail} + {rightPurchaseActions} +
+
+
+ +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee new file mode 100644 index 000000000..16d63fdef --- /dev/null +++ b/web/app/assets/javascripts/react-components/LessonSession.js.jsx.coffee @@ -0,0 +1,58 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@LessonSession = React.createClass({ + + mixins: [ + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + onAppInit: (@app) -> + @app.bindScreen('jamclass/lesson-session', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + + @setState({user: userState?.user}) + + componentDidMount: () -> + + @root = $(@getDOMNode()) + + getInitialState: () -> + { + user: null, + lesson: null, + updating: false, + } + + beforeHide: (e) -> + @resetErrors() + + beforeShow: (e) -> + + afterShow: (e) -> + @setState({updating: true}) + + + render: () -> + header = "header" + + `
+
+ {header} + +
+
+ +
+ +
+ +
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee b/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee new file mode 100644 index 000000000..f0c9d7730 --- /dev/null +++ b/web/app/assets/javascripts/react-components/Nav.js.jsx.coffee @@ -0,0 +1,34 @@ +context = window +teacherActions = window.JK.Actions.Teacher + +@Nav = React.createClass({ + + mixins: [ + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(NavStore, "onNavChanged") + ] + + onAppInit: (@app) -> + + onNavChanged: (nav) -> + @setState({nav: nav}) + + render: () -> + navs = [] + if this.state?.nav? + nav = this.state.nav + if nav.currentSection? + navs.push(` : `) + navs.push(`{nav.currentSection.name}`) + if nav.optionalParent? + navs.push(` : `) + navs.push(`{nav.optionalParent.name}`) + if nav.currentScreenName? + navs.push(` : `) + navs.push(`{nav.currentScreenName}`) + + `
+ JamKazam Home + {navs} +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee index e6a097dcd..79c17b269 100644 --- a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee @@ -39,18 +39,18 @@ logger = context.JK.logger @props.onItemChanged(country, region) render: () -> - countries = [``] + countries = [``] for countryId, countryInfo of @state.countries - countries.push(``) + countries.push(``) country = @state.countries[@state.selectedCountry] - regions = [``] + regions = [``] if country? && country.regions for region in country.regions - regions.push(``) + regions.push(``) disabled = regions.length == 1 `
diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index c214aab20..5a4450313 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -160,7 +160,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds SessionActions.downloadingJamTrack(false) - console.log("JamTrackPlay: result", ) + console.log("JamTrackPlay: result", result) if !result @app.notify( { diff --git a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee new file mode 100644 index 000000000..b6ef5d193 --- /dev/null +++ b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee @@ -0,0 +1,40 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@StripeConnect = React.createClass({ + + + getInitialState: () -> + { + clicked:false + } + + onStripeConnect: (e) -> + + if this.state.clicked + return + + e.preventDefault() + this.setState({clicked: true}) + StripeActions.connect(this.props.purpose) + + render: () -> + + if this.props.user?.stripe_auth? + return `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
` + + if this.state.clicked + imageUrl = '/assets/content/stripe-connect-light-on-dark.png' + else + imageUrl = '/assets/content/stripe-connect-blue-on-dark.png' + + `
+ + +
` + + +}) diff --git a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee index e6f720de5..28b9f2378 100644 --- a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee @@ -60,7 +60,7 @@ logger = context.JK.logger render: () -> endDate = [] if this.props.showEndDate - endDate.push ` + endDate.push ` ` dtLabel = "Start & End" else 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 ca93b873e..45861f89f 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -1,5 +1,4 @@ context = window -MIX_MODES = context.JK.MIX_MODES rest = context.JK.Rest() logger = context.JK.logger @@ -251,7 +250,7 @@ proficiencyDescriptionMap = {

{this.state.user.first_name} Teaches {this.editProfileLink('edit teaching', 'basics')}

- +
{teachesInfo} diff --git a/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee index f1c3e09ce..9a0d77fb1 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchOptionsScreen.js.jsx.coffee @@ -44,7 +44,8 @@ LocationActions = @LocationActions onSearch: (e) -> e.preventDefault() - TeacherSearchActions.search({searchOptions: @state.options}) + logger.debug("teacher search options", @state.options) + TeacherSearchActions.search(@state.options) levelChanged: (e) -> @@ -67,7 +68,7 @@ LocationActions = @LocationActions selectedAge = null yearsTeaching = [] for yr in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 45, 50] - yearsTeaching.push(``) + yearsTeaching.push(``) `

search teachers

@@ -100,15 +101,15 @@ LocationActions = @LocationActions
- +
- +
- +
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 ab918bd0f..2db743425 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -15,10 +15,16 @@ ProfileActions = @ProfileActions # Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchChanged"), mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged"), - Reflux.listenTo(@TeacherSearchResultsStore, "onTeacherSearchResultsStore")] + Reflux.listenTo(@TeacherSearchResultsStore, "onTeacherSearchResultsStore"), + Reflux.listenTo(@TeacherSearchStore, "onTeacherSearchStore")] LIMIT: 20 visible: false + needToSearch: true + root: null + endOfList: null + contentBodyScroller: null + refreshing: false getInitialState: () -> {searchOptions: {}, results: []} @@ -32,10 +38,25 @@ ProfileActions = @ProfileActions afterShow: (e) -> @visible = false #@setState(TeacherSearchStore.getState()) - TeacherSearchResultsActions.reset() + #if @state.results.length == 0 + # don't issue a new search every time someone comes to the screen, to preserve location from previous browsing + if @needToSearch + @contentBodyScroller.off('scroll') + @endOfList.hide() + + TeacherSearchResultsActions.reset() + @needToSearch = false afterHide: (e) -> + onTeacherSearchStore: (storeChanged) -> + @needToSearch = true + + onTeacherSearchResultsStore: (results) -> + results.searching = false + @refreshing = false + @contentBodyScroller.find('.infinite-scroll-loader-2').remove() + @setState(results) onUserChanged: (@user) -> @@ -46,6 +67,24 @@ ProfileActions = @ProfileActions componentDidMount: () -> @root = $(@getDOMNode()) @resultsNode = @root.find('.results') + @endOfList = @root.find('.end-of-teacher-list') + @contentBodyScroller = @root + + registerInfiniteScroll:() -> + $scroller = @contentBodyScroller + logger.debug("registering infinite scroll") + $scroller.off('scroll') + $scroller.on('scroll', () => + # be sure to not fire off many refreshes when user hits the bottom + return if @refreshing + + if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight + $scroller.append('
... Loading more Teachers ...
') + @refreshing = true + @setState({searching: true}) + logger.debug("refreshing more teachers for infinite scroll") + TeacherSearchResultsActions.nextPage() + ) componentDidUpdate: () -> @resultsNode.find('.teacher-bio').each((index, element) => ( @@ -55,17 +94,51 @@ ProfileActions = @ProfileActions after: "a.readmore" }) )) - onTeacherSearchResultsStore: (results) -> - @setState(results) + + + if @state.next == null + @contentBodyScroller.off('scroll') + if @state.currentPage == 1 and @state.results.length == 0 + @endOfList.text('No Teachers found matching your search').show() + logger.debug("TeacherSearch: empty search") + else if @state.currentPage > 0 + logger.debug("end of search") + @endOfList.text('No more Teachers').show() + else + @registerInfiniteScroll(@contentBodyScroller) moreAboutTeacher: (user, e) -> e.preventDefault() ProfileActions.viewTeacherProfile(user, '/client#/teachers/search', 'BACK TO TEACHER SEARCH') - bookTestDrive: (e) -> + bookTestDrive: (user, e) -> e.preventDefault() + rest.getTestDriveStatus({id: context.JK.currentUserId, teacher_id: user.id}) + .done((response) => + if response.remaining_test_drives == 0 && response['can_buy_test_drive?'] + logger.debug("TeacherSearchScreen: user offered test drive") + @app.layout.showDialog('try-test-drive', {d1: user.teacher.id}) + else if response.remaining_test_drives > 0 + if response.booked_with_teacher && !context.JK.currentUserAdmin + logger.debug("TeacherSearchScreen: teacher already test-drived") + + context.JK.Banner.showAlert('TestDrive', "You have already take a TestDrive lesson from this teacher. With TestDrive, you need to use your lessons on 4 different teachers to find one who is best for you. We're sorry, but you cannot take multiple TestDrive lessons from a single teacher.") + else + # send on to booking screen for this teacher + logger.debug("TeacherSearchScreen: user being sent to book a lesson") + + window.location.href = '/client#/jamclass/book-lesson/test-drive_' + user.id + else + # user has no remaining test drives and can't buy any + logger.debug("TeacherSearchScreen: test drive all done") + context.JK.Banner.showAlert('TestDrive', "You have already taken advantage of the TestDrive program within the past year, so we are sorry, but you are not eligible to use TestDrive again now. You may book a normal lesson with this teacher by closing this message, and clicking the BOOK LESSON button for this teacher.") + ) + .fail((jqXHR, textStatus, errorMessage) => + @app.ajaxError(jqXHR, textStatus, errorMessage) + ) + bookNormalLesson: (e) -> e.preventDefault() @@ -82,7 +155,7 @@ ProfileActions = @ProfileActions teacherBio.css('height', 'auto') createSearchDescription: () -> - searchOptions = TeacherSearchStore.getState().searchOptions + searchOptions = TeacherSearchStore.getState() summary = '' instruments = searchOptions.instruments @@ -158,8 +231,7 @@ ProfileActions = @ProfileActions @@ -169,15 +241,17 @@ ProfileActions = @ProfileActions `
- LESSONS HOME :  - TEACHERS SEARCH :  + JamKazam Home :  + JamClass Home :  + Teachers Search :  - SEARCH RESULTS / + Search Results / {searchDesc}
{resultsJsx} +
No more Teachers
` diff --git a/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee new file mode 100644 index 000000000..aff9f15fa --- /dev/null +++ b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee @@ -0,0 +1,99 @@ +context = window +ConfigureTracksStore = @ConfigureTracksStore +ConfigureTracksActions = @ConfigureTracksActions +@TryTestDriveDialog = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + teacher: null + + beforeShow: (args) -> + # d1 should be a teacher ID (vs a user ID that happens to be a teacher) + logger.debug("TryTestDriveDialog.beforeShow", args.d1) + @teacher = args.d1 + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('try-test-drive', dialogBindings); + + getInitialState: () -> + {} + + doCancel: (e) -> + e.preventDefault() + @app.layout.closeDialog('try-test-drive', true); + + doTestDriveNow: (e) -> + e.preventDefault() + + rest.createTeacherIntent({id: @teacher, intent: 'book-test-drive'}).done((response) => @marked(response)).fail((jqXHR) => @failedMark(jqXHR)) + + marked: () -> + @app.layout.closeDialog('try-test-drive') + window.location.href = "/client#/jamclass/lesson-payment" + + failedMark: (jqXHR, textStatus, errorMessage) -> + @app.ajaxError(jqXHR, textStatus, errorMessage) + + render: () -> + `
+
+ + +

TestDrive

+
+
+ +

What Is TestDrive?

+ +

+ Our TestDrive package is the best way to painlessly find the best instructor for you - i.e. a teacher who has + the qualifications you're looking for, and with whom you really connect. This is the most critical factor in + achieving the results you want from your investment of time and money into lessons.

+ +

With TestDrive you pay just $49.99 to take 4 full lessons - one lesson with each of 4 different teachers you + select. Then you can pick the teacher who works best for you. It's an amazing value, highly convenient, and + the best way to find the right teacher for you.

+ +

If you're serious about getting started on lessons, this is the way to go.

+ + +
+
` + + inputChanged: (e) -> + $root = $(@getDOMNode()) + + deletePath: (path, e) -> + e.preventDefault() + + ConfigureTracksActions.removeSearchPath(path) + + selectVSTDirectory: (e) -> + e.preventDefault() + + ConfigureTracksActions.selectVSTDirectory() + + doClose: (e) -> + e.preventDefault() + + @app.layout.closeDialog('try-test-drive', true) + + componentDidMount: () -> + $root = $(@getDOMNode()) + + componentWillUpdate: () -> + @ignoreICheck = true + $root = $(@getDOMNode()) + + componentDidUpdate: () -> + $root = $(@getDOMNode()) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee new file mode 100644 index 000000000..315f3193e --- /dev/null +++ b/web/app/assets/javascripts/react-components/UploadAvatarDialog.js.jsx.coffee @@ -0,0 +1,46 @@ +context = window + +@UploadAvatarDialog = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + + + beforeShow: (args) -> + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('upload-avatar', dialogBindings); + + getInitialState: () -> + {} + + render: () -> + `
+
+ + +

Update Logo

+
+
+ + + +
+
` + + + componentDidMount: () -> + $root = $(@getDOMNode()) + + componentWillUpdate: () -> + $root = $(@getDOMNode()) + + componentDidUpdate: () -> + $root = $(@getDOMNode()) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee new file mode 100644 index 000000000..130a2f213 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee @@ -0,0 +1,10 @@ +context = window + +@AvatarActions = Reflux.createActions({ + start: {} + select: {} + pick: {} + update: {} + delete: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee new file mode 100644 index 000000000..5e3828859 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee @@ -0,0 +1,7 @@ +context = window + +@NavActions = Reflux.createActions({ + setScreenInfo: {} + screenChanged: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee new file mode 100644 index 000000000..8977445d2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@SchoolActions = Reflux.createActions({ + refresh: {}, + addInvitation: {}, + deleteInvitation: {} + updateSchool: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee new file mode 100644 index 000000000..33d27d6e1 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee @@ -0,0 +1,6 @@ +context = window + +@StripeActions = Reflux.createActions({ + connect: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee index 924187040..7b39d44e5 100644 --- a/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee @@ -2,5 +2,6 @@ context = window @TeacherSearchResultsActions = Reflux.createActions({ reset: {} + nextPage: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee index daf5708ab..815277def 100644 --- a/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee @@ -3,5 +3,6 @@ context = window @UserActions = Reflux.createActions({ loaded: {} modify: {} + refresh: {} }) diff --git a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee new file mode 100644 index 000000000..204353571 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee @@ -0,0 +1,394 @@ +context = window +rest = context.JK.Rest() + +@JamClassAffiliateLandingBottomPage = React.createClass({ + + render: () -> + `
+
+

What Is The JamKazam Affiliate Program?

+ +

If you’re a music store and you sell products like instruments and accessories to musicians, + + JamKazam can add value to every sale you make to customers by enabling you to bundle high-value digital music products and services that are highly complementary to the products you + + are selling – at no additional cost to your customers. For example, you can bundle full + + multitrack recordings from our catalog of 4,000+ popular songs that let your customers play + + along with their favorite songs in incredibly innovative and fun ways, and you can + bundle free + + online music lessons that really work (unlike Skype, which is great for voice chat but horrible for + + music lessons) to help your customers connect with great teachers and stay better engaged + + with their instruments, so they’ll keep coming back for more music products from your store. + + Additionally, when your customers love these digital products and services and choose to buy + + more, we pay you a share of all revenues we earn from these sales, boosting your revenue and + + profit per sale, while simultaneously delivering greater value to your customer.

+ +

If you’re a music school, JamKazam represents a platform far superior to Skype that you can use + + freely to teach your existing students online, both in your area and across the country to reach + + new markets and students. Skype’s audio quality for music is very poor. Its latency is so high + + that teacher and student cannot play together, and it suffers from a number of other critical + + limitations for music lessons, as it was built for voice chat – not music. In addition to delivering + + online music lessons that really work, JamKazam can drive more students to your school + and + + your teachers from our online lesson marketplace, helping you build your business faster and + + bigger.

+ +

And if you’re both a music store and school, we can help you win in all these ways together. If + + this sounds interesting, read on to learn more about some of the unique things we’ve done and + + how they can help to grow your business.

+ +
+

JamTracks Kudos

+
+ + + +

Andy Crowley of AndyGuitar

+
+
+ + + +

Ryan Jones of PianoKeyz

+
+
+ + + +

Carl Brown of GuitarLessions365

+
+
+
+ +
+

JamTracks – The Best Way To Play With Your Favorite Songs

+ +

+ JamTracks are full multitrack recordings of 4,000+ popular songs that deliver amazing fun and + + flexibility for musicians to play with their favorite artists and tunes. With JamTracks, you can: +

+
    +
  • Listen to just a single isolated part to learn it
  • +
  • Mute the part you want to play, and play along with the rest of the band
  • +
  • Slow down playback to practice without changing the pitch
  • +
  • Change the song key by raising or lowering pitch in half steps
  • +
  • Save custom mixes for easy access, and export them to use anywhere
  • +
  • Make audio and video recordings to share via Facebook, YouTube, etc.
  • +
  • Play online live and in sync with others from different locations
  • +
  • Apply VST & AU audio plugin effects to your live performance
  • +
  • Use MIDI with VST & AU instruments for keys, electronic drums, etc.
  • +
  • And more…
  • +
+

+ JamTracks sell for $1.99 each. Musicians love to play with these, and typically buy a few at a + + time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a + + JamKazam affiliate, you can bundle free JamTracks with the strings sale, delighting your + + customer with the added value. And then when he/she buys more JamTracks, we pay you a + + share of that revenue, adding to your revenues, and improving your margin on your sales. This + + can be applied to just about anything you sell, as we have JamTracks with parts for almost every + + instrument, as well as vocals. And it’s easy to bundle, as you don’t have to specify particular + + songs per product that you sell. You can just give your customer a free JamTracks credit with + + each sale, and your customer can then redeem this credit online to choose their favorite songs. +

+ +

+ Here is a video that shows more about how JamTracks work. +

+
+