From f6d5b520fb0c3c20de610159ea39ab6c910d5087 Mon Sep 17 00:00:00 2001
From: Seth Call
Date: Wed, 17 Feb 2016 15:44:57 -0600
Subject: [PATCH] * wip
---
db/up/lessons.sql | 43 +-
ruby/Gemfile | 2 +
ruby/lib/jam_ruby.rb | 2 +
ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 40 +
.../student_lesson_request.html.erb | 11 +
.../student_lesson_request.text.erb | 3 +
.../teacher_lesson_request.html.erb | 11 +
.../teacher_lesson_request.text.erb | 3 +
ruby/lib/jam_ruby/lib/stats.rb | 28 +-
ruby/lib/jam_ruby/models/lesson_booking.rb | 63 +-
.../jam_ruby/models/lesson_booking_slot.rb | 1 +
.../models/lesson_package_purchase.rb | 28 +-
.../jam_ruby/models/lesson_package_type.rb | 24 +
ruby/lib/jam_ruby/models/lesson_session.rb | 38 +-
ruby/lib/jam_ruby/models/sale.rb | 96 +-
ruby/lib/jam_ruby/models/sale_line_item.rb | 11 +-
ruby/lib/jam_ruby/models/shopping_cart.rb | 4 +
ruby/lib/jam_ruby/models/teacher.rb | 15 +
ruby/lib/jam_ruby/models/user.rb | 59 +
ruby/spec/factories.rb | 49 +
.../jam_ruby/models/lesson_booking_spec.rb | 26 +-
.../models/lesson_package_purchase_spec.rb | 3 +-
.../jam_ruby/models/lesson_session_spec.rb | 39 +
ruby/spec/jam_ruby/models/sale_spec.rb | 78 +
ruby/spec/jam_ruby/models/user_spec.rb | 31 +
ruby/spec/mailers/render_emails_spec.rb | 19 +
ruby/spec/spec_helper.rb | 2 +
ruby/spec/support/utilities.rb | 12 +
web/Gemfile | 7 +-
web/app/assets/javascripts/application.js | 1 +
web/app/assets/javascripts/jam_rest.js | 21 +
.../BookLessonFree.js.jsx.coffee | 1 +
.../FreeLessonPayment.js.jsx.coffee | 139 -
.../JamClassStudentScreen.js.jsx.coffee | 154 +
.../LessonPayment.js.jsx.coffee | 324 +-
.../actions/UserActions.js.coffee | 1 +
.../mixins/ICheckMixin.js.coffee | 40 +
.../stores/UserStore.js.coffee | 7 +
.../client/jamtrack_landing.css.scss | 2 +
.../FreeLessonPayment.css.scss | 100 -
.../JamClassStudentScreen.css.scss | 86 +
.../react-components/LessonPayment.css.scss | 105 +
web/app/controllers/api_controller.rb | 4 +
.../api_lesson_bookings_controller.rb | 12 +-
.../api_lesson_sessions_controller.rb | 14 +
web/app/controllers/api_stripe_controller.rb | 11 +
web/app/helpers/client_helper.rb | 1 +
web/app/views/api_jamblasters/get_tokens.rabl | 2 +-
web/app/views/api_lesson_bookings/show.rabl | 4 +-
web/app/views/api_lesson_sessions/index.rabl | 11 +
web/app/views/api_lesson_sessions/show.rabl | 31 +
web/app/views/api_stripe/store.rabl | 14 +
web/app/views/api_users/show.rabl | 4 +-
web/app/views/clients/index.html.erb | 2 +-
.../jamclass/_free_lesson_payment.html.slim | 10 -
.../jamclass/_jamclass_student.html.slim | 10 +
.../jamclass/_lesson_payment.html.slim | 2 +-
web/app/views/errors/stripe_error.rabl | 13 +
web/app/views/layouts/client.html.erb | 1 +
web/app/views/shared/_stripe.html.slim | 6 +
web/config/application.rb | 11 +-
web/config/initializers/stripe.rb | 1 +
web/config/initializers/zip_codes.rb | 1 +
web/config/routes.rb | 3 +
.../assets/javascripts/jquery.inputmask.js | 2653 +++++++++++++++++
.../assets/javascripts/jquery.payment.js | 651 ++++
.../lib/jam_websockets/router.rb | 1 -
67 files changed, 4872 insertions(+), 330 deletions(-)
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/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/spec/jam_ruby/models/lesson_session_spec.rb
delete mode 100644 web/app/assets/javascripts/react-components/FreeLessonPayment.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/mixins/ICheckMixin.js.coffee
delete mode 100644 web/app/assets/stylesheets/client/react-components/FreeLessonPayment.css.scss
create mode 100644 web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss
create mode 100644 web/app/controllers/api_lesson_sessions_controller.rb
create mode 100644 web/app/controllers/api_stripe_controller.rb
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_stripe/store.rabl
delete mode 100644 web/app/views/clients/jamclass/_free_lesson_payment.html.slim
create mode 100644 web/app/views/clients/jamclass/_jamclass_student.html.slim
create mode 100644 web/app/views/errors/stripe_error.rabl
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/vendor/assets/javascripts/jquery.inputmask.js
create mode 100644 web/vendor/assets/javascripts/jquery.payment.js
diff --git a/db/up/lessons.sql b/db/up/lessons.sql
index 97cc823b0..a47d253f0 100644
--- a/db/up/lessons.sql
+++ b/db/up/lessons.sql
@@ -19,16 +19,39 @@ CREATE TABLE lesson_package_purchases (
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,
+ lesson_type VARCHAR(64) NOT NULL,
+ recurring BOOLEAN NOT NULL,
+ lesson_length INTEGER NOT NULL,
+ payment_style VARCHAR(64) NOT NULL,
+ description VARCHAR,
+ 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,
+ 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(),
music_session_id VARCHAR(64) REFERENCES music_sessions(id) NOT NULL,
lesson_type VARCHAR(64) NOT NULL,
- teacher_id VARCHAR(64) REFERENCES teachers(id) 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,
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,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -42,19 +65,6 @@ INSERT INTO lesson_package_types (id, name, description, package_type, price) VA
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_bookings (
- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
- user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
- lesson_type VARCHAR(64) NOT NULL,
- recurring BOOLEAN NOT NULL,
- lesson_length INTEGER NOT NULL,
- payment_style VARCHAR(64) NOT NULL,
- description VARCHAR,
- teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
- card_presumed_ok BOOLEAN NOT NULL DEFAULT FALSE,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
CREATE TABLE lesson_booking_slots (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
@@ -64,6 +74,7 @@ CREATE TABLE lesson_booking_slots (
day_of_week INTEGER,
hour INTEGER,
minute INTEGER,
+ timezone VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -73,3 +84,7 @@ ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES le
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);
diff --git a/ruby/Gemfile b/ruby/Gemfile
index 0680c23cb..561c4efa8 100644
--- a/ruby/Gemfile
+++ b/ruby/Gemfile
@@ -52,6 +52,8 @@ gem 'sanitize'
gem 'influxdb', '0.1.8'
gem 'recurly'
gem 'sendgrid_toolkit', '>= 1.1.1'
+gem 'stripe'
+gem 'zip-codes'
group :test do
gem 'simplecov', '~> 0.7.1'
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index fa7619211..817f0f91e 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -21,6 +21,8 @@ require 'rest-client'
require 'zip'
require 'csv'
require 'tzinfo'
+require 'stripe'
+require 'zip-codes'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
index 84355a1ae..ec73a6d9f 100644
--- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
+++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
@@ -621,6 +621,46 @@
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 send_notification(email, subject, msg, unique_args)
# @body = msg
# sendgrid_category "Notification"
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..ab60f3f5b
--- /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 Request sent to #{@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 reject 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..068228c3e
--- /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.home_url %>
\ No newline at end of file
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..e3f0e537a
--- /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/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/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb
index 993590e45..c340a0d65 100644
--- a/ruby/lib/jam_ruby/models/lesson_booking.rb
+++ b/ruby/lib/jam_ruby/models/lesson_booking.rb
@@ -4,6 +4,13 @@ module JamRuby
@@log = Logging.logger[LessonBooking]
+ STATUS_REQUESTED = 'requested'
+ STATUS_CANCELED = 'canceled'
+ STATUS_MISSED = 'missed'
+ STATUS_COMPLETED = 'completed'
+
+ STATUS_TYPES = [STATUS_REQUESTED, STATUS_CANCELED, STATUS_MISSED, STATUS_COMPLETED]
+
LESSON_TYPE_FREE = 'single-free'
LESSON_TYPE_TEST_DRIVE = 'test-drive'
LESSON_TYPE_PAID = 'paid'
@@ -20,11 +27,15 @@ module JamRuby
belongs_to :user, class_name: "JamRuby::User"
belongs_to :teacher, class_name: "JamRuby::User"
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
+ has_many :lesson_sessions, class_name: "JamRuby::LessonSession"
validates :user, presence: true
validates :teacher, presence: true
validates :lesson_type, presence: true, 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 :lesson_length, presence: true, inclusion: {in: [30, 45, 60, 90, 120]}
validates :payment_style, inclusion: {in: PAYMENT_STYLES}
validates :description, no_profanity: true, length: {minimum: 10, maximum: 20000}, presence: true
@@ -35,6 +46,40 @@ module JamRuby
validate :validate_lesson_length
validate :validate_payment_style
+ after_create :after_create
+ def after_create
+ if card_presumed_ok && !sent_notices
+ send_notices
+ end
+ end
+
+ def send_notices
+ UserMailer.student_lesson_request(self).deliver
+ UserMailer.teacher_lesson_request(self).deliver
+ LessonBooking.where(id: id).update_all(sent_notices: true)
+ end
+
+ def display_type
+ if is_single_free?
+ "Free"
+ elsif is_test_drive?
+ "TestDrive"
+ elsif is_normal?
+ "Lesson Purchase"
+ end
+ end
+
+ # determine the price of this booking based on what the user wants, and the teacher's pricing
+ def booked_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 is_single_free?
lesson_type == LESSON_TYPE_FREE
end
@@ -47,6 +92,13 @@ module JamRuby
lesson_type == LESSON_TYPE_PAID
end
+ def card_approved
+ LessonBooking.where(id: id).update_all(card_presumed_ok: true)
+ if !sent_notices
+ send_notices
+ end
+ end
+
def validate_user
if is_single_free?
if !user.has_free_lessons?
@@ -124,6 +176,7 @@ module JamRuby
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.lesson_booking_slots = lesson_booking_slots
@@ -131,6 +184,7 @@ module JamRuby
lesson_booking.lesson_length = lesson_length
lesson_booking.payment_style = payment_style
lesson_booking.description = description
+ lesson_booking.status = STATUS_REQUESTED
if lesson_booking.save
@@ -152,8 +206,15 @@ module JamRuby
end
def self.unprocessed(current_user)
- LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false).first
+ LessonBooking.where(user_id: current_user.id).where(card_presumed_ok: false)
end
+ def home_url
+ APP_CONFIG.external_root_url + "/client#/jamclass"
+ end
+
+ def web_url
+ APP_CONFIG.external_root_url + "/client#/jamclass/lesson-request/" + 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
index ae286d5e3..564c86d3d 100644
--- a/ruby/lib/jam_ruby/models/lesson_booking_slot.rb
+++ b/ruby/lib/jam_ruby/models/lesson_booking_slot.rb
@@ -16,6 +16,7 @@ module JamRuby
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
validate :validate_slot_type
diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
index d255a0398..88bff5b92 100644
--- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
+++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
@@ -9,16 +9,40 @@ module JamRuby
end
# who purchased the lesson package?
- belongs_to :user, class_name: "JamRuby::User"
+ 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::Teacher"
- def self.create(user, lesson_package_type)
+ validates :lesson_package_type, presence: true
+ validates :price, presence: true
+
+ after_save :after_save
+
+ def after_save
+
+ 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 = user.remaining_test_drives + 4
+ end
+
+ end
+
+ def self.create(user, lesson_package_type, lesson_booking)
purchase = LessonPackagePurchase.new
purchase.user = user
purchase.lesson_package_type = lesson_package_type
+ purchase.price = lesson_package_type.booked_price(lesson_booking)
purchase.save
purchase
end
+
+ def price_in_cents
+ (price * 100).to_i
+ end
+
+ def description(lesson_booking)
+ lesson_package_type.description(lesson_booking)
+ 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
index 298007853..054619b69 100644
--- a/ruby/lib/jam_ruby/models/lesson_package_type.rb
+++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb
@@ -34,6 +34,30 @@ module JamRuby
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.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 recurring
+ "Recurring #{lesson_booking.payment_style == PAYMENT_STYLE_WEEKLY ? "Weekly" : "Monthly"} #{lesson_booking.lesson_length}m"
+ else
+ "Single #{lesson_booking.lesson_length}m lesson"
+ end
+ end
+ end
+
def is_single_free?
id == SINGLE_FREE
end
diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb
index b149cc2dd..bcf313d29 100644
--- a/ruby/lib/jam_ruby/models/lesson_session.rb
+++ b/ruby/lib/jam_ruby/models/lesson_session.rb
@@ -10,14 +10,50 @@ module JamRuby
LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE]
belongs_to :music_session, class_name: "JamRuby::MusicSession"
- belongs_to :teacher, class_name: "JamRuby::Teacher"
+ belongs_to :teacher, class_name: "JamRuby::User"
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
+ belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
validates :duration, presence: true, numericality: {only_integer: true}
+ validates :lesson_booking, presence: true
validates :lesson_type, inclusion: {in: LESSON_TYPES}
validates :price, presence: true
validates :teacher_complete, inclusion: {in: [true, false]}
validates :student_complete, inclusion: {in: [true, false]}
+ validates :teacher_cancelled, inclusion: {in: [true, false]}
+ validates :student_cancelled, inclusion: {in: [true, false]}
+
+ 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
end
end
diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb
index 7fb4e073a..d70507367 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'
belongs_to :user, class_name: 'JamRuby::User'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
@@ -32,12 +33,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
@@ -76,7 +77,7 @@ module JamRuby
def self.validateIOSReceipt(receipt)
# these are all 'in cents' (as painfully named to be very clear), and all expected to be integers
- price_info = {subtotal_in_cents:nil, total_in_cents:nil, tax_in_cents:nil, currency: 'USD'}
+ price_info = {subtotal_in_cents: nil, total_in_cents: nil, tax_in_cents: nil, currency: 'USD'}
# communicate with Apple; populate price_info
@@ -180,6 +181,75 @@ module JamRuby
free && non_free
end
+ def self.purchase_test_drive(current_user)
+ self.purchase_lesson(current_user, LessonPackageType.test_drive)
+ end
+
+ # this is easy to make generic, but right now, it just purchases lessons
+ def self.purchase_lesson(current_user, lesson_package_type)
+ sale = nil
+ # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
+ Sale.transaction do
+
+ sale = create_lesson_sale(current_user)
+
+ if sale.valid?
+
+ price_info = charge_stripe_for_lesson(current_user, lesson_package_type)
+
+ SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type)
+
+ # 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
+ 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
+ end
+
+
+ def self.charge_stripe_for_lesson(current_user, lesson_package_type, lesson_booking = nil)
+ current_user.sync_stripe_customer
+
+ purchase = LessonPackagePurchase.create(current_user, lesson_package_type, lesson_booking)
+
+ 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
+
+ charge = Stripe::Charge.create(
+ :amount => total_in_cents,
+ :currency => "usd",
+ :customer => current_user.stripe_customer_id,
+ :description => purchase.description(lesson_booking)
+ )
+
+ 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] = charge.id
+ 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)
@@ -338,7 +408,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)
@@ -454,6 +523,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 = Sale.new
sale.user = user
@@ -463,6 +536,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..91365820a 100644
--- a/ruby/lib/jam_ruby/models/sale_line_item.rb
+++ b/ruby/lib/jam_ruby/models/sale_line_item.rb
@@ -5,6 +5,7 @@ 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'
@@ -13,7 +14,7 @@ module JamRuby
belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id
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,6 +82,14 @@ module JamRuby
line_item
end
+ # in a shopping-cart less world (ios purchase), let's reuse as much logic as possible
+ def self.create_from_lesson_package(current_user, sale, lesson_package_type)
+ shopping_cart = ShoppingCart.create(current_user, lesson_package_type, 1)
+ line_item = create_from_shopping_cart(sale, shopping_cart, nil, nil, nil)
+ 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)
product_info = shopping_cart.product_info
diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb
index fb531c2bd..d531daf8c 100644
--- a/ruby/lib/jam_ruby/models/shopping_cart.rb
+++ b/ruby/lib/jam_ruby/models/shopping_cart.rb
@@ -115,6 +115,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/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb
index 8b6a8e394..22c9b86cb 100644
--- a/ruby/lib/jam_ruby/models/teacher.rb
+++ b/ruby/lib/jam_ruby/models/teacher.rb
@@ -235,6 +235,21 @@ module JamRuby
teacher
end
+ def booking_price(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.to_i
+ 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/user.rb b/ruby/lib/jam_ruby/models/user.rb
index f0743a4de..a13e51fb0 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -170,6 +170,9 @@ 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
+
# Shopping carts
has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart"
@@ -1841,6 +1844,62 @@ module JamRuby
remaining_test_drives > 0
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 => "JK ID: #{id}",
+ :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_lesson = nil
+ User.transaction do
+ self.stripe_token = token
+ self.stripe_zip_code = zip
+ customer = sync_stripe_customer
+ self.stripe_customer_id = customer.id
+ if self.save
+ # we can also 'unlock' any booked sessions that still need to be done so
+ LessonBooking.unprocessed(self).each do |lesson|
+ approved_lesson = lesson.card_approved
+ end
+ end
+ end
+ approved_lesson
+ end
+
+ def payment_update(params)
+ lesson = nil
+ test_drive = nil
+ User.transaction do
+ lesson = card_approved(params[:token], params[:zip])
+ if params[:test_drive]
+ test_drive = Sale.purchase_test_drive(self)
+ end
+ end
+
+ {lesson: lesson, test_drive: test_drive}
+ end
+
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index b67d87169..2123e2508 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -95,6 +95,11 @@ 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)
+ end
+ end
end
factory :teacher, :class => JamRuby::Teacher do
@@ -149,6 +154,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
@@ -910,6 +918,7 @@ FactoryGirl.define do
sequence(:sibling_key ) { |n| "sibling_key#{n}" }
end
+
factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do
factory :lesson_booking_slot_single do
slot_type 'single'
@@ -917,6 +926,7 @@ FactoryGirl.define do
day_of_week nil
hour 12
minute 30
+ timezone 'UTC'
end
factory :lesson_booking_slot_recurring do
@@ -925,8 +935,47 @@ FactoryGirl.define do
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
+ lesson_booking_slots [FactoryGirl.build(:lesson_booking_slot_single), FactoryGirl.build(:lesson_booking_slot_single)]
+ end
+
+ factory :lesson_package_purchase, class: "JamRuby::LessionPackagePurchase" do
+ lesson_package_type { JamRuby::LessonPackageType.single }
+ association :user, factory: :user
+ association :teacher, factory: :teacher
+ price 30.00
+ 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
+ price 49.99
+ #teacher_complete true
+ #student_complete true
+ end
factory :ip_blacklist, class: "JamRuby::IpBlacklist" do
remote_ip '1.1.1.1'
diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb
index 62c19df41..9bd9706c7 100644
--- a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb
+++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
# collissions with teacher's schedule?
describe LessonBooking do
- let(:user) {FactoryGirl.create(:user, stored_credit_card: true, remaining_free_lessons: 1, remaining_test_drives: 1)}
+ let(:user) {FactoryGirl.create(:user, stored_credit_card: false, remaining_free_lessons: 1, remaining_test_drives: 1)}
let(:teacher) {FactoryGirl.create(:teacher)}
let(:teacher_user) {teacher.user}
let(:lesson_booking_slot_single1) {FactoryGirl.build(:lesson_booking_slot_single)}
@@ -33,6 +33,16 @@ describe LessonBooking do
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
@@ -55,7 +65,7 @@ describe LessonBooking do
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 ["has no remaining free lessons"]
+ booking.errors[:user].should eq ["have no remaining free lessons"]
ChatMessage.count.should eq 1
end
@@ -65,8 +75,7 @@ describe LessonBooking do
user.save!
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 ["has no credit card stored"]
+ booking.errors.any?.should be false
end
it "must have 2 lesson booking slots" do
@@ -127,7 +136,7 @@ describe LessonBooking do
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 no remaining test drives"]
+ booking.errors[:user].should eq ["have no remaining test drives"]
ChatMessage.count.should eq 1
end
@@ -221,13 +230,14 @@ describe LessonBooking do
ChatMessage.count.should eq 2
end
- it "prevents user without stored credit card" do
+ 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 true
- booking.errors[:user].should eq ["has no credit card stored"]
+ booking.errors.any?.should be false
+ booking.card_presumed_ok.should eq false
+ booking.sent_notices.should eq false
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
index ab05738d1..485fd4810 100644
--- a/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb
+++ b/ruby/spec/jam_ruby/models/lesson_package_purchase_spec.rb
@@ -3,9 +3,10 @@ require 'spec_helper'
describe LessonPackagePurchase do
let(:user) {FactoryGirl.create(:user)}
+ let(:lesson_booking) {FactoryGirl.create(:lesson_booking)}
it "creates" do
- purchase = LessonPackagePurchase.create(user, LessonPackageType.single_free)
+ purchase = LessonPackagePurchase.create(user, LessonPackageType.single_free, lesson_booking)
purchase.valid?.should be_true
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..3c07eaa2f
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb
@@ -0,0 +1,39 @@
+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(:lesson_session) {FactoryGirl.create(:lesson_session, student: user, teacher: teacher)}
+ let(:lesson_session2) {FactoryGirl.create(:lesson_session, student: user, teacher: teacher)}
+ describe "index" do
+ it "finds single lesson as student" 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(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/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb
index fc89145cb..42814de4b 100644
--- a/ruby/spec/jam_ruby/models/sale_spec.rb
+++ b/ruby/spec/jam_ruby/models/sale_spec.rb
@@ -567,6 +567,84 @@ describe Sale do
end
end
+ describe "purchase_test_drive" do
+
+ it "book single" do
+
+ end
+
+ it "book recurring, single" do
+
+ end
+
+ it "book recurring, monthly" do
+
+ end
+
+ it "can succeed" do
+ user.stripe_token = create_stripe_token
+ user.save!
+
+ sale = Sale.purchase_test_drive(user)
+
+ sale.reload
+
+ 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 4
+ 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
+ end
+
+ it "can succeed with tax" do
+ user.stripe_token = create_stripe_token
+ user.stripe_zip_code = '78759'
+ user.save!
+
+ sale = Sale.purchase_test_drive(user)
+
+ sale.reload
+
+ 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
+ user.remaining_test_drives.should eql 4
+ 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
+ end
+ end
+
describe "check_integrity_of_jam_track_sales" do
let(:user) { FactoryGirl.create(:user) }
diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb
index 89abe6e16..ff2753057 100644
--- a/ruby/spec/jam_ruby/models/user_spec.rb
+++ b/ruby/spec/jam_ruby/models/user_spec.rb
@@ -760,6 +760,37 @@ describe User do
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
+
=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..e1d554f06 100644
--- a/ruby/spec/mailers/render_emails_spec.rb
+++ b/ruby/spec/mailers/render_emails_spec.rb
@@ -39,6 +39,25 @@ describe "RenderMailers", :slow => true do
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}
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 = FactoryGirl.create(:lesson_booking)
+ UserMailer.teacher_lesson_request(lesson_booking).deliver
+ end
+
+ it "student_lesson_request" do
+ @filename = "student_lesson_request"
+
+ lesson_booking = FactoryGirl.create(:lesson_booking)
+ UserMailer.student_lesson_request(lesson_booking).deliver
+ end
+ end
end
describe "InvitedUserMailer emails" do
diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb
index 0f28a82de..45efd9da8 100644
--- a/ruby/spec/spec_helper.rb
+++ b/ruby/spec/spec_helper.rb
@@ -60,6 +60,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'
diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb
index 193f5ed76..1ca51cc02 100644
--- a/ruby/spec/support/utilities.rb
+++ b/ruby/spec/support/utilities.rb
@@ -258,6 +258,7 @@ def app_config
true
end
+
private
def audiomixer_workspace_path
@@ -328,3 +329,14 @@ def friend(user1, user2)
FactoryGirl.create(:friendship, user: user1, friend: user2)
FactoryGirl.create(:friendship, user: user2, friend: user1)
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
\ No newline at end of file
diff --git a/web/Gemfile b/web/Gemfile
index a2054a95c..0a59a5a3a 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -59,7 +59,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 +88,14 @@ 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 "browserify-rails", "~> 0.7"
source 'https://rails-assets.org' do
diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js
index 019ddfcda..6b8e803af 100644
--- a/web/app/assets/javascripts/application.js
+++ b/web/app/assets/javascripts/application.js
@@ -39,6 +39,7 @@
//= require jquery.payment
//= require jquery.visible
//= require jquery.jstarbox
+//= require jquery.inputmask
//= require fingerprint2.min
//= require ResizeSensor
//= require classnames
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index 5138ee5a5..031e5cb8c 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -2142,6 +2142,25 @@
});
}
+ function submitStripe(options) {
+ return $.ajax({
+ type: "POST",
+ url: '/api/stripe',
+ dataType: "json",
+ contentType: 'application/json',
+ data: JSON.stringify(options)
+ })
+ }
+
+ function getLessonSessions(options) {
+ return $.ajax({
+ type: "GET",
+ url: "/api/lesson_sessions?" + $.param(query),
+ dataType: "json",
+ contentType: 'application/json'
+ });
+ }
+
function initialize() {
return self;
}
@@ -2335,6 +2354,8 @@
this.portOverCarts = portOverCarts;
this.bookLesson = bookLesson;
this.getUnprocessedLesson = getUnprocessedLesson;
+ this.submitStripe = submitStripe;
+ this.getLessonSessions = getLessonSessions;
return this;
};
})(window,jQuery);
diff --git a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee
index c54a98f74..2f337ff57 100644
--- a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee
@@ -115,6 +115,7 @@ UserStore = context.UserStore
options.payment_style = 'elsewhere'
options.lesson_type = 'single-free'
options.slots = [@getSlotData(0), @getSlotData(1)]
+ options.timezone = Ajstz.determine().name()
description = @root.find('textarea.user-description').val()
if description == ''
description == null
diff --git a/web/app/assets/javascripts/react-components/FreeLessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/FreeLessonPayment.js.jsx.coffee
deleted file mode 100644
index e661d746f..000000000
--- a/web/app/assets/javascripts/react-components/FreeLessonPayment.js.jsx.coffee
+++ /dev/null
@@ -1,139 +0,0 @@
-context = window
-rest = context.JK.Rest()
-logger = context.JK.logger
-
-UserStore = context.UserStore
-
-@FreeLessonPayment = React.createClass({
-
- mixins: [
- Reflux.listenTo(AppStore, "onAppInit"),
- Reflux.listenTo(UserStore, "onUserChanged")
- ]
-
- onAppInit: (@app) ->
- @app.bindScreen('jamclass/free-lesson-payment',
- {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) ->
- @resetState()
- @resetErrors()
- @setState({updating:true})
- rest.getUnprocessedLesson().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR))
-
- resetState: () ->
- @setState({update: false, lesson: null})
-
- unprocessLoaded: (response) ->
- @setState({updating: false})
- @setState({lesson: response})
-
- 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()
-
-
- onSubmit: (e) ->
- e.preventDefault()
-
-
- render: () ->
-
- disabled = @state.updating
-
- if @state.updating
- photo_url = '/assets/shared/avatar_generic.png'
- name = 'Loading ...'
- teacherDetails = `
-
-

-
- {name}
-
`
- else
- if @state.lesson?
- photo_url = @state.lesson.teacher.photo_url
- name = @state.lesson.teacher.name
- if !photo_url?
- photo_url = '/assets/shared/avatar_generic.png'
- teacherDetails = `
-
-

-
- {name}
-
`
-
- if lesson.lesson_type == 'single-free'
- bookingInfo = `You are booking a single free {this.state.lesson.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.lesson_type == 'test-drive'
- bookingInfo = `This is not the correct page to pay for TestDrive.
`
- bookingDetail = ''
- else if lesson.lesson_type == 'paid'
- bookingInfo = `This is not the correct page for entering pay for a normal lesson.
`
- bookingDetail = ''
-
- `
-
-
- {teacherDetails}
-
- {bookingInfo}
-
- {bookingDetail}
-
-
-
-
-
`
-
-})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee
new file mode 100644
index 000000000..5657d5ef7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee
@@ -0,0 +1,154 @@
+context = window
+rest = context.JK.Rest()
+logger = context.JK.logger
+
+UserStore = context.UserStore
+
+@JamClassStudentScreen = React.createClass({
+
+ mixins: [
+ @ICheckMixin,
+ Reflux.listenTo(AppStore, "onAppInit"),
+ Reflux.listenTo(UserStore, "onUserChanged")
+ ]
+
+ onAppInit: (@app) ->
+ @app.bindScreen('jamclass',
+ {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
+
+ onUserChanged: (userState) ->
+ @setState({user: userState?.user})
+
+ componentDidMount: () ->
+
+ componentDidUpdate: () ->
+
+ getInitialState: () ->
+ {
+ user: null,
+ }
+
+ beforeHide: (e) ->
+ @resetErrors()
+
+ beforeShow: (e) ->
+
+ afterShow: (e) ->
+ @setState({updating: true})
+ rest.getLessonSessions().done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR))
+
+ resetState: () ->
+ @setState({updating: false, lesson: null})
+
+ jamClassLoaded: (response) ->
+ @setState({updating: false})
+ @setState({summary: response})
+
+ failedJamClassLoad: (jqXHR) ->
+ @setState({updating: false})
+ @setState({summary: response})
+ if jqXHR.status == 404
+ @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"})
+
+ render: () ->
+ disabled = @state.updating
+
+ classes = []
+ if @state.updating
+ classes = [`| Loading... |
`]
+ else
+
+ `
+
+
+
my lessons
+
+
+
+ | TEACHER |
+ DATE/TIME |
+ STATUS |
+ |
+ ACTIONS |
+
+
+
+ {classes}
+
+
+
+
+
+
+
search teachers
+
+
JamClass instructors are each individually screened to ensure that they are highly qualified music
+ teachers,
+ equipped to teach effectively online, and background checked.
+
+
+
+
+
+
+
+
learn about jamclass
+
+
+ JamClass is the best way to make music lessons, offering significant advantadges over both traditional
+ face-to-face lessons
+ and online skype lessons.
+
+
+
+
+
+
sign up for testdrive
+
+
+ There are two awesome, painless ways to get started with JamClass.
+
+
+
+ Sign up for TestDrive and take 4 full 30-minute lessons - one each from 4 different instructors - for just
+ $49.99.
+ You wouldn't marry the first person you date, right? Find the best teacher for you. It's the most important
+ factor in the success for your lessons!
+
+
+
+ Or take one JamClass lesson free. It's on us! We're confident you'll take more.
+
+
+
+ Sign up for TestDrive using the button below, or to take one free lesson, search our teachers, and click the
+ Book Free Lesson on your favorite.
+
+
+
+
+
+
get ready for your first lesson
+
+
Be sure to set up and test the JamKazam app in an online music session a few days before
+ your first lesson! We're happy to help, and we'll even get in a session with you to make sure everything
+ is working properly. Ping us at support@jamkazam.com anytime, and
+ read our
+ JamClass user guide to learn how to use all the lesson features.
+
+
+
+
+
`
+
+})
\ 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
index cf04951d7..5be92e02e 100644
--- a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee
@@ -7,23 +7,36 @@ UserStore = context.UserStore
@LessonPayment = React.createClass({
mixins: [
+ @ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(UserStore, "onUserChanged")
]
onAppInit: (@app) ->
- @app.bindScreen('jamclass/payment',
+ @app.bindScreen('jamclass/lesson-payment',
{beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
onUserChanged: (userState) ->
@setState({user: userState?.user})
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}
+ {
+ user: null,
+ lesson: null,
+ updating: false,
+ billingInUS: true
+ }
beforeHide: (e) ->
@resetErrors()
@@ -31,15 +44,165 @@ UserStore = context.UserStore
beforeShow: (e) ->
afterShow: (e) ->
+ @resetState()
@resetErrors()
- rest.getUnprocessedLesson().done((response) => @booked(response)).fail((jqXHR) => @failedBooking(jqXHR))
+ @setState({updating: true})
+ rest.getUnprocessedLesson().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR))
+
+ resetErrors: () ->
+ @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null})
+
+ checkboxChanged: (e) ->
+ checked = $(e.target).is(':checked')
+
+ @setState({billingInUS: checked})
+
+ resetState: () ->
+ @setState({updating: false, lesson: null})
+
+ unprocessLoaded: (response) ->
+ @setState({updating: false})
+ @setState({lesson: 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()
+
+ 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
+ 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 !$.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
+ if this.state.billingInUS
+ zip = @root.find('input.zip').val()
+
+ rest.submitStripe({
+ token: response.id,
+ zip: zip,
+ test_drive: @state.lesson?.lesson_type == 'test-drive'
+ }).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
+
+ stripeSubmitted: (response) ->
+ logger.debug("stripe submitted", response)
+
+ # 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-request/" + response.lesson.id
+ else if response.test_drive?
+ context.Banner.showNotice({
+ title: "Test Drive Purchased",
+ 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."
+ })
+ window.location = "/client#/teachers/search"
+ else
+ window.location = "/client#/teachers/search"
+
+ stripeSubmitFailure: (jqXHR) ->
+ @app.layout.notifyServerError(jqXHR, 'Credit Card Not Stored')
render: () ->
+ disabled = @state.updating
if @state.updating
+ photo_url = '/assets/shared/avatar_generic.png'
+ name = 'Loading ...'
+ teacherDetails = `
+
+

+
+ {name}
+
`
else
if @state.lesson?
photo_url = @state.lesson.teacher.photo_url
+ name = @state.lesson.teacher.name
if !photo_url?
photo_url = '/assets/shared/avatar_generic.png'
teacherDetails = `
@@ -50,50 +213,149 @@ UserStore = context.UserStore
`
if lesson.lesson_type == 'single-free'
- bookingInfo = `This is not the correct page to pay for TestDrive.
`
+ header = `enter card info
+
+
Your card wil not be charged.
See explanation to the right.
+
`
+ bookingInfo = `You are booking a single free {this.state.lesson.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.lesson_type == 'test-drive'
- bookingInfo = `This is not the correct page to pay for TestDrive.
`
+ 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.lesson_type == 'paid'
- bookingInfo = `You are booking a {this.state.lesson.lesson_length} minute lesson for ${this.state.lesson.booked_price.toFixed(2)}
`
+ header = `enter payment info for lesson
`
+ if this.state.lesson.recurring
+ if this.state.lesson.payment_style == 'single'
+ bookingInfo = `You are booking a {this.state.lesson.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 {this.state.lesson.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 {this.state.lesson.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 {this.state.lesson.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}
+ backClassNames = {'button-grey': true, disabled: disabled}
+ 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}
`
{teacherDetails}
-
{bookingInfo}
+ {bookingInfo}
-
BOOKING DETAIL TODO
-
-
-
+ {bookingDetail}
+
+
+
`
})
\ No newline at end of file
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/mixins/ICheckMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee
new file mode 100644
index 000000000..c07ca1ec1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee
@@ -0,0 +1,40 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@ICheckMixin = {
+
+ iCheckIgnore: false
+ checkboxes: []
+
+ iCheckify: () ->
+ @setCheckboxState()
+ @enableICheck()
+
+ setCheckboxState: () ->
+ for checkbox in @checkboxes
+ selector = checkbox.selector
+ stateKey = checkbox.stateKey
+ enabled = @state[stateKey]
+
+ @iCheckIgnore = true
+ if enabled
+ @root.find(selector).iCheck('check').attr('checked', true);
+ else
+ @root.find(selector).iCheck('uncheck').attr('checked', false);
+ @iCheckIgnore = false
+
+ enableICheck: (e) ->
+ checkboxes = @root.find('input[type="checkbox"]')
+ context.JK.checkbox(checkboxes)
+ checkboxes.on('ifChanged', (e) => @checkIfCanFire(e))
+ true
+
+ checkIfCanFire: (e) ->
+ if @iCheckIgnore
+ return
+
+ if @checkboxChanged?
+ @checkboxChanged(e)
+ else
+ logger.warn("no checkbox changed implemented")
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee b/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee
index 5aaa11202..02b7d7a3a 100644
--- a/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee
@@ -1,6 +1,7 @@
$ = jQuery
context = window
logger = context.JK.logger
+rest = context.JK.Rest
@UserStore = Reflux.createStore(
{
@@ -26,6 +27,12 @@ logger = context.JK.logger
@user = $.extend({}, @user, changes)
@changed()
+ onRefresh: () ->
+ rest.getUserDetail().done((response) => @onLoaded(response)).fail((jqXHR) => @onUserFail(jqXHR))
+
+ onUserFail:(jqXHR) ->
+ @app.layout.notify({title: 'Unable to Update User Info', text: "We recommend you refresh the page."})
+
changed:() ->
@trigger({user: @user})
diff --git a/web/app/assets/stylesheets/client/jamtrack_landing.css.scss b/web/app/assets/stylesheets/client/jamtrack_landing.css.scss
index 238c58190..a32770611 100644
--- a/web/app/assets/stylesheets/client/jamtrack_landing.css.scss
+++ b/web/app/assets/stylesheets/client/jamtrack_landing.css.scss
@@ -65,6 +65,8 @@
tbody {
}
+
+ margin-bottom:20px;
}
.search-area {
diff --git a/web/app/assets/stylesheets/client/react-components/FreeLessonPayment.css.scss b/web/app/assets/stylesheets/client/react-components/FreeLessonPayment.css.scss
deleted file mode 100644
index 4499dfff8..000000000
--- a/web/app/assets/stylesheets/client/react-components/FreeLessonPayment.css.scss
+++ /dev/null
@@ -1,100 +0,0 @@
-@import "client/common";
-
-#free-lesson-payment {
-
- .content-body-scroller {
- height:100%;
- padding:30px;
- }
-
- h2 {
- font-size: 20px;
- font-weight:700;
- margin-bottom: 20px !important;
- display:inline-block;
- }
- .no-charge {
- float:right;
- }
- .column {
- @include border_box_sizing;
- width:50%;
- }
- .column-left {
- float:left;
- padding-right:20px;
- }
- .column-right {
- float:right;
- padding-left:20px;
- }
- label {
- display:inline-block;
- }
- select {
- display:inline-block;
- }
-
- input {
- display:inline-block;
- width: calc(100% - 150px);
- @include border_box_sizing;
- }
- textarea {
- width:100%;
- @include border_box_sizing;
- height:125px;
- }
- .field {
- position:relative;
- display:block;
- margin-top:15px;
- margin-bottom:25px;
-
- label {
- width:150px;
- }
- }
- p {
- line-height:125% !important;
- font-size:14px !important;
- margin:0 0 20px 0 !important;
- }
- .avatar {
- display:inline-block;
- padding:1px;
- width:48px;
- height:48px;
- background-color:#ed4818;
- margin:10px 20px 0 0;
- -webkit-border-radius:24px;
- -moz-border-radius:24px;
- border-radius:24px;
- float:none;
- }
- .avatar img {
- width: 48px;
- height: 48px;
- -webkit-border-radius:24px;
- -moz-border-radius:24px;
- border-radius:24px;
- }
- .teacher-name {
- font-size:16px;
- display:inline-block;
- height:48px;
- vertical-align:middle;
- }
- .jamclass-policies {
- text-align:center;
- margin-top:-20px;
- }
- .actions {
- margin-left:-3px;
- margin-bottom:20px;
- }
- .error-text {
- display:block;
- }
-
-}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss b/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss
new file mode 100644
index 000000000..d56e04dcf
--- /dev/null
+++ b/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss
@@ -0,0 +1,86 @@
+@import "client/common";
+
+#jam-class-student-screen {
+
+ div[data-react-class="JamClassStudentScreen"] {
+ height:100%;
+ }
+ .content-body-scroller {
+ height:100%;
+ padding:30px;
+ @include border_box_sizing;
+ }
+
+ h2 {
+ font-size: 20px;
+ font-weight:700;
+ margin-bottom: 20px !important;
+ display:inline-block;
+ }
+ .column {
+ @include border_box_sizing;
+ width:50%;
+ }
+ .column-left {
+ float:left;
+ padding-right:20px;
+ }
+ .column-right {
+ float:right;
+ padding-left:20px;
+ }
+ p {
+ line-height:125% !important;
+ font-size:14px !important;
+ margin:0 0 20px 0 !important;
+ color: $ColorTextTypical;
+ }
+ .avatar {
+ display:inline-block;
+ padding:1px;
+ width:36px;
+ height:36px;
+ background-color:#ed4818;
+ margin:10px 20px 0 0;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+ float:none;
+ }
+ .avatar img {
+ width: 36px;
+ height: 36px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+ }
+
+ .calender-integration-notice {
+ display:block;
+ text-align:center;
+ }
+ .actions {
+ display:block;
+ text-align:center;
+ }
+ .jamclass-section {
+ margin-bottom:40px;
+ }
+ .jamtable {
+ a {
+ text-decoration: underline !important;
+ color:#fc0 !important;
+ }
+ th {
+ font-size:14px;
+ padding:3px 10px;
+ }
+ td {
+ padding:4px 15px;
+ font-size:14px;
+ }
+ tbody {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/react-components/LessonPayment.css.scss b/web/app/assets/stylesheets/client/react-components/LessonPayment.css.scss
index 9136c89d9..5c13b48e6 100644
--- a/web/app/assets/stylesheets/client/react-components/LessonPayment.css.scss
+++ b/web/app/assets/stylesheets/client/react-components/LessonPayment.css.scss
@@ -2,4 +2,109 @@
#lesson-payment {
+ div[data-react-class="LessonPayment"] {
+ height:100%;
+ }
+ .content-body-scroller {
+ height:100%;
+ padding:30px;
+ @include border_box_sizing;
+ }
+
+ h2 {
+ font-size: 20px;
+ font-weight:700;
+ margin-bottom: 20px !important;
+ display:inline-block;
+ }
+ .no-charge {
+ float:right;
+ }
+ .column {
+ @include border_box_sizing;
+ width:50%;
+ }
+ .column-left {
+ float:left;
+ padding-right:20px;
+ }
+ .column-right {
+ float:right;
+ padding-left:20px;
+ }
+ label {
+ display:inline-block;
+ }
+ select {
+ display:inline-block;
+ }
+
+ input {
+ display:inline-block;
+ width: calc(100% - 150px);
+ @include border_box_sizing;
+ }
+ textarea {
+ width:100%;
+ @include border_box_sizing;
+ height:125px;
+ }
+ .field {
+ position:relative;
+ display:block;
+ margin-top:15px;
+ margin-bottom:25px;
+
+ label {
+ width:150px;
+ }
+ }
+ p {
+ line-height:125% !important;
+ font-size:14px !important;
+ margin:0 0 20px 0 !important;
+ }
+ .avatar {
+ display:inline-block;
+ padding:1px;
+ width:48px;
+ height:48px;
+ background-color:#ed4818;
+ margin:10px 20px 0 0;
+ -webkit-border-radius:24px;
+ -moz-border-radius:24px;
+ border-radius:24px;
+ float:none;
+ }
+ .avatar img {
+ width: 48px;
+ height: 48px;
+ -webkit-border-radius:24px;
+ -moz-border-radius:24px;
+ border-radius:24px;
+ }
+ .teacher-name {
+ font-size:16px;
+ display:inline-block;
+ height:48px;
+ vertical-align:middle;
+ }
+ .jamclass-policies {
+ display:block;
+ text-align:center;
+ }
+ .actions {
+ margin-left:-3px;
+ margin-bottom:20px;
+ }
+ .error-text {
+ display:block;
+ }
+
+ .actions {
+ float:left;
+ clear:both;
+ }
+
+
}
\ No newline at end of file
diff --git a/web/app/controllers/api_controller.rb b/web/app/controllers/api_controller.rb
index e202c509e..94ed0a4a4 100644
--- a/web/app/controllers/api_controller.rb
+++ b/web/app/controllers/api_controller.rb
@@ -23,6 +23,10 @@ class ApiController < ApplicationController
@exception = exception
render "errors/conflict_error", :status => 409
end
+ rescue_from 'Stripe::StripeError' do |exception|
+ @exception = exception
+ render "errors/stripe_error", :status => 422
+ end
rescue_from 'ActiveRecord::RecordNotFound' do |exception|
log.debug(exception)
render :json => { :errors => { :resource => ["record not found"] } }, :status => 404
diff --git a/web/app/controllers/api_lesson_bookings_controller.rb b/web/app/controllers/api_lesson_bookings_controller.rb
index 55d107162..60d683380 100644
--- a/web/app/controllers/api_lesson_bookings_controller.rb
+++ b/web/app/controllers/api_lesson_bookings_controller.rb
@@ -3,6 +3,15 @@ class ApiLessonBookingsController < ApiController
before_filter :api_signed_in_user
respond_to :json
+ def index
+ data = LessonBooking.index(current_user)
+
+ @lessons = data[:query]
+
+ @next = data[:next_page]
+ render "api_lesson_bookings/index", :layout => nil
+ end
+
def create
if params[:lesson_type] == LessonBooking::LESSON_TYPE_FREE
@@ -28,6 +37,7 @@ class ApiLessonBookingsController < ApiController
specified_slot.preferred_day = day
specified_slot.hour = slot[:hour]
specified_slot.minute = slot[:minute]
+ specified_slot.timezone = slot[:timezone]
slots << specified_slot
end
@lesson_booking = LessonBooking.book_free(current_user, teacher, slots, params[:description])
@@ -50,6 +60,6 @@ class ApiLessonBookingsController < ApiController
def unprocessed
@show_teacher = true
- @lesson_booking = LessonBooking.unprocessed(current_user)
+ @lesson_booking = LessonBooking.unprocessed(current_user).first
end
end
diff --git a/web/app/controllers/api_lesson_sessions_controller.rb b/web/app/controllers/api_lesson_sessions_controller.rb
new file mode 100644
index 000000000..0001d9757
--- /dev/null
+++ b/web/app/controllers/api_lesson_sessions_controller.rb
@@ -0,0 +1,14 @@
+class ApiLessonSessionsController < ApiController
+
+ before_filter :api_signed_in_user
+ respond_to :json
+
+ def index
+ data = LessonSession.index(current_user, params)
+
+ @lesson_sessions = data[:query]
+
+ @next = data[:next_page]
+ render "api_lesson_sessions/index", :layout => nil
+ end
+end
diff --git a/web/app/controllers/api_stripe_controller.rb b/web/app/controllers/api_stripe_controller.rb
new file mode 100644
index 000000000..efd3c339f
--- /dev/null
+++ b/web/app/controllers/api_stripe_controller.rb
@@ -0,0 +1,11 @@
+class ApiStripeController < ApiController
+
+ before_filter :api_signed_in_user
+ respond_to :json
+
+ def store
+ data = user.payment_update(params)
+ @lesson = data[:lesson]
+ @test_drive = data[:test_drive]
+ end
+end
diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb
index 151d2394d..01a73152e 100644
--- a/web/app/helpers/client_helper.rb
+++ b/web/app/helpers/client_helper.rb
@@ -81,5 +81,6 @@ module ClientHelper
gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores
gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos
+ gon.stripe_publishable_key = Rails.application.config.stripe_publishable_key
end
end
diff --git a/web/app/views/api_jamblasters/get_tokens.rabl b/web/app/views/api_jamblasters/get_tokens.rabl
index 6fdfa70ef..c30dd828b 100644
--- a/web/app/views/api_jamblasters/get_tokens.rabl
+++ b/web/app/views/api_jamblasters/get_tokens.rabl
@@ -1,4 +1,4 @@
object @jamblasters
-attributes :id, :serial_no, :client_id, :vtoken
\ No newline at end of file
+attributes :id, :serial_no, :client_id, :vtoken, :key
\ No newline at end of file
diff --git a/web/app/views/api_lesson_bookings/show.rabl b/web/app/views/api_lesson_bookings/show.rabl
index e2e17e76b..368d7b88b 100644
--- a/web/app/views/api_lesson_bookings/show.rabl
+++ b/web/app/views/api_lesson_bookings/show.rabl
@@ -10,6 +10,6 @@ child(:user => :user) {
attributes :id, :has_stored_credit_card?
}
-child (:teacher => :teacher) { |teacher|
+child(:teacher => :teacher) do |teacher|
partial "api_users/show", object: teacher
-}
\ No newline at end of file
+end
\ No newline at end of file
diff --git a/web/app/views/api_lesson_sessions/index.rabl b/web/app/views/api_lesson_sessions/index.rabl
new file mode 100644
index 000000000..76ebfcd45
--- /dev/null
+++ b/web/app/views/api_lesson_sessions/index.rabl
@@ -0,0 +1,11 @@
+node :next do |page|
+ @next
+end
+
+node :entries do |page|
+ partial "api_lesson_sessions/show", object: @lesson_sessions
+end
+
+node :total_entries do |page|
+ @lesson_sessions.total_entries
+end
diff --git a/web/app/views/api_lesson_sessions/show.rabl b/web/app/views/api_lesson_sessions/show.rabl
new file mode 100644
index 000000000..2fe2c97c5
--- /dev/null
+++ b/web/app/views/api_lesson_sessions/show.rabl
@@ -0,0 +1,31 @@
+object @lesson_session
+
+attributes :id, :lesson_type, :duration, :price, :teacher_complete, :student_complete, :status, :student_canceled, :teacher_canceled, :student_canceled_at, :teacher_canceled_at, :student_canceled_reason, :teacher_canceled_reason, :status
+
+child(:music_session => :music_session) {
+ attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat,
+ :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :play_count, :scheduled_duration,
+ :language, :recurring_mode, :language_description, :scheduled_start_date, :access_description, :timezone, :timezone_id, :timezone_description,
+ :musician_access_description, :fan_access_description, :session_removed_at, :legal_policy, :open_rsvps, :is_unstructured_rsvp?
+
+
+ node :scheduled_start_date do |session|
+ scheduled_start_date(session)
+ end
+
+ node :scheduled_start do |history|
+ history.scheduled_start_time.strftime("%a %e %B %Y %H:%M:%S") if history.scheduled_start
+ end
+
+ node :pretty_scheduled_start_with_timezone do |session|
+ pretty_scheduled_start(session, true)
+ end
+
+ node :pretty_scheduled_start_short do|session|
+ pretty_scheduled_start(session, false)
+ end
+}
+
+child(:teacher => :teacher) do |teacher|
+ partial "api_users/show", object: teacher
+end
\ No newline at end of file
diff --git a/web/app/views/api_stripe/store.rabl b/web/app/views/api_stripe/store.rabl
new file mode 100644
index 000000000..e83d57c81
--- /dev/null
+++ b/web/app/views/api_stripe/store.rabl
@@ -0,0 +1,14 @@
+object @lesson
+
+node :lesson do |lesson|
+ attribute :id
+end
+
+if @test_drive
+ node :test_drive do |lesson|
+
+ end
+end
+
+
+
diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl
index e36546085..28d10dd10 100644
--- a/web/app/views/api_users/show.rabl
+++ b/web/app/views/api_users/show.rabl
@@ -31,7 +31,7 @@ end
# give back more info if the user being fetched is yourself
if current_user && @user == current_user
- attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack
+ attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack, :remaining
node :geoiplocation do |user|
geoiplocation = current_user.geoiplocation
@@ -68,7 +68,7 @@ if current_user && @user == current_user
if @show_student
node :has_unprocessed_lesson do |user|
- !!LessonBooking.unprocessed(user)
+ !!LessonBooking.unprocessed(user).first
end
end
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb
index 491e8f8e6..0d1fb6ecc 100644
--- a/web/app/views/clients/index.html.erb
+++ b/web/app/views/clients/index.html.erb
@@ -47,7 +47,7 @@
<%= render "clients/teachers/search/search_results" %>
<%= render "clients/jamclass/book_lesson_free" %>
<%= render "clients/jamclass/lesson_payment" %>
-<%= render "clients/jamclass/free_lesson_payment" %>
+<%= render "clients/jamclass/jamclass_student" %>
<%= render "users/feed_music_session_ajax" %>
<%= render "users/feed_recording_ajax" %>
<%= render "jamtrack_search" %>
diff --git a/web/app/views/clients/jamclass/_free_lesson_payment.html.slim b/web/app/views/clients/jamclass/_free_lesson_payment.html.slim
deleted file mode 100644
index 33294c50d..000000000
--- a/web/app/views/clients/jamclass/_free_lesson_payment.html.slim
+++ /dev/null
@@ -1,10 +0,0 @@
-#free-lesson-payment.screen.secondary layout="screen" layout-id="jamclass/free-lesson-payment"
- .content-head
- .content-icon
- = image_tag "content/icon_account.png", :size => "27x20"
- h1
- | jamclass
- = render "screen_navigation"
- .content-body
- = react_component 'FreeLessonPayment', {}
-
diff --git a/web/app/views/clients/jamclass/_jamclass_student.html.slim b/web/app/views/clients/jamclass/_jamclass_student.html.slim
new file mode 100644
index 000000000..3607f633c
--- /dev/null
+++ b/web/app/views/clients/jamclass/_jamclass_student.html.slim
@@ -0,0 +1,10 @@
+#jam-class-student-screen.screen.secondary layout="screen" layout-id="jamclass"
+ .content-head
+ .content-icon
+ = image_tag "content/icon_jamtracks.png", :size => "24x24"
+ h1
+ | jamclass
+ = render "screen_navigation"
+ .content-body
+ = react_component 'JamClassStudentScreen', {}
+
diff --git a/web/app/views/clients/jamclass/_lesson_payment.html.slim b/web/app/views/clients/jamclass/_lesson_payment.html.slim
index 0eee834e3..6cfa5917d 100644
--- a/web/app/views/clients/jamclass/_lesson_payment.html.slim
+++ b/web/app/views/clients/jamclass/_lesson_payment.html.slim
@@ -1,4 +1,4 @@
-#lesson-payment.screen.secondary layout="screen" layout-id="jamclass/payment" layout-arg="id"
+#lesson-payment.screen.secondary layout="screen" layout-id="jamclass/lesson-payment"
.content-head
.content-icon
= image_tag "content/icon_account.png", :size => "27x20"
diff --git a/web/app/views/errors/stripe_error.rabl b/web/app/views/errors/stripe_error.rabl
new file mode 100644
index 000000000..53b717210
--- /dev/null
+++ b/web/app/views/errors/stripe_error.rabl
@@ -0,0 +1,13 @@
+object @exception
+
+node do |exception|
+ errors = {}
+ errors["message"] = [exception.to_s]
+ {
+ errors: errors
+ }
+end
+
+node "type" do
+ "StripeError"
+end
\ No newline at end of file
diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb
index 1908a054d..b77ca9351 100644
--- a/web/app/views/layouts/client.html.erb
+++ b/web/app/views/layouts/client.html.erb
@@ -24,6 +24,7 @@
<%= yield %>
<%= render "shared/ga" %>
<%= render "shared/recurly" %>
+ <%= render "shared/stripe" %>
<%= render "shared/google_nocaptcha" %>
<%= render "shared/olark" %>