* wip
This commit is contained in:
parent
f42b390c50
commit
f6d5b520fb
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<% provide(:title, "Lesson Request sent to #{@sender.name}") %>
|
||||
<% provide(:photo_url, @sender.resolved_photo_url) %>
|
||||
|
||||
<% content_for :note do %>
|
||||
<p>You have requested a <%= @lesson_booking.display_type %> lesson. <br /><br/>Click the button below to see your lesson request. You will receive another email when the teacher accepts or reject the request.</p>
|
||||
<p>
|
||||
<a href="<%= @lesson_booking.home_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">VIEW LESSON REQUEST</a>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
You have requested a lesson from <%= @sender.name %>.
|
||||
|
||||
To see this lesson request, click here: <%= @lesson_booking.home_url %>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<% provide(:title, "Lesson Request from #{@sender.name}") %>
|
||||
<% provide(:photo_url, @sender.resolved_photo_url) %>
|
||||
|
||||
<% content_for :note do %>
|
||||
<p>This student has requested to schedule a <%= @lesson_booking.display_type %> lesson. <br /><br/>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!</p>
|
||||
<p>
|
||||
<a href="<%= @lesson_booking.home_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">VIEW LESSON REQUEST</a>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= @sender.name %> has requested a lesson.
|
||||
|
||||
To see this lesson request, click here: <%= @lesson_booking.home_url %>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
//= require jquery.payment
|
||||
//= require jquery.visible
|
||||
//= require jquery.jstarbox
|
||||
//= require jquery.inputmask
|
||||
//= require fingerprint2.min
|
||||
//= require ResizeSensor
|
||||
//= require classnames
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = `<div className="teacher-header">
|
||||
<div className="avatar">
|
||||
<img src={photo_url}/>
|
||||
</div>
|
||||
{name}
|
||||
</div>`
|
||||
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 = `<div className="teacher-header">
|
||||
<div className="avatar">
|
||||
<img src={photo_url}/>
|
||||
</div>
|
||||
{name}
|
||||
</div>`
|
||||
|
||||
if lesson.lesson_type == 'single-free'
|
||||
bookingInfo = `<p>You are booking a single free {this.state.lesson.lesson_length}-minute lesson.</p>`
|
||||
bookingDetail = `<p>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.
|
||||
<br/>
|
||||
|
||||
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></div>
|
||||
</p>`
|
||||
else if lesson.lesson_type == 'test-drive'
|
||||
bookingInfo = `<p>This is not the correct page to pay for TestDrive.</p>`
|
||||
bookingDetail = ''
|
||||
else if lesson.lesson_type == 'paid'
|
||||
bookingInfo = `<p>This is not the correct page for entering pay for a normal lesson.</p>`
|
||||
bookingDetail = ''
|
||||
|
||||
`<div className="content-body-scroller">
|
||||
<div className="column column-left">
|
||||
<h2>enter card info</h2><div className="no-charge">Your card wil not be charged.<br/>See explanation to the right.</div>
|
||||
|
||||
<div className="field card-number">
|
||||
<label>Card Number:</label>
|
||||
<input disabled={disabled} type="text" name="card-number"></input>
|
||||
</div>
|
||||
<div className="field expiration">
|
||||
<label>Expiration Date:</label>
|
||||
<input disabled={disabled} type="text" name="expiration"></input>
|
||||
</div>
|
||||
<div className="field cvv">
|
||||
<label>CVV:</label>
|
||||
<input disabled={disabled} type="text" name="cvv"></input>
|
||||
</div>
|
||||
|
||||
<div className="field zip">
|
||||
<label>Zip/Postal Code:</label>
|
||||
<input disabled={disabled} type="text" name="zip"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column column-right">
|
||||
{teacherDetails}
|
||||
<div className="booking-info">
|
||||
{bookingInfo}
|
||||
|
||||
{bookingDetail}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<a className={backClasses} onClick={this.onBack}>BACK</a><a className={submitClasses} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
|
||||
</div>
|
||||
|
||||
</div>`
|
||||
|
||||
})
|
||||
|
|
@ -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 = [`<tr><td colspan="5">Loading...</td></tr>`]
|
||||
else
|
||||
|
||||
`<div className="content-body-scroller">
|
||||
<div className="column column-left">
|
||||
<div className="my-lessons jamclass-section">
|
||||
<h2>my lessons</h2>
|
||||
<table className="jamtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TEACHER</th>
|
||||
<th>DATE/TIME</th>
|
||||
<th>STATUS</th>
|
||||
<th></th>
|
||||
<th>ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{classes}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="calender-integration-notice">
|
||||
Don't miss a lesson! <a>Integrate your lessons into your calendar.</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="search-teachers">
|
||||
<h2>search teachers</h2>
|
||||
|
||||
<p>JamClass instructors are each individually screened to ensure that they are highly qualified music
|
||||
teachers,
|
||||
equipped to teach effectively online, and background checked.
|
||||
</p>
|
||||
|
||||
<div className="actions">
|
||||
<a href="/client#/teachers/search" className="button-orange">SEARCH TEACHERS</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column column-right">
|
||||
<div className="jamclass-section">
|
||||
<h2>learn about jamclass</h2>
|
||||
|
||||
<p>
|
||||
JamClass is the best way to make music lessons, offering significant advantadges over both traditional
|
||||
face-to-face lessons
|
||||
and online skype lessons.
|
||||
</p>
|
||||
|
||||
<div className="actions">
|
||||
<a href="/landing/jamclass/students" className="button-orange">LEARN MORE</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="jamclass-section">
|
||||
<h2>sign up for testdrive</h2>
|
||||
|
||||
<p>
|
||||
There are two awesome, painless ways to get started with JamClass.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Or take one JamClass lesson free. It's on us! We're confident you'll take more.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="actions">
|
||||
<a href="/landing/jamclass/students" className="button-orange">SIGN UP FOR TESTDRIVE</a>
|
||||
or
|
||||
<a href="/client#/teachers/search" className="button-orange">SEARCH TEACHERS</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="jamclass-section">
|
||||
<h2>get ready for your first lesson</h2>
|
||||
|
||||
<p>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 <a href="mailto:support@jamkazam.com">support@jamkazam.com anytime</a>, and
|
||||
read our
|
||||
<a onClick={alert.bind('not yet')}>JamClass user guide</a> to learn how to use all the lesson features.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>`
|
||||
|
||||
})
|
||||
|
|
@ -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.<br/><br/>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.<br/><br/>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 = `<div className="teacher-header">
|
||||
<div className="avatar">
|
||||
<img src={photo_url}/>
|
||||
</div>
|
||||
{name}
|
||||
</div>`
|
||||
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 = `<div className="teacher-header">
|
||||
|
|
@ -50,50 +213,149 @@ UserStore = context.UserStore
|
|||
</div>`
|
||||
|
||||
if lesson.lesson_type == 'single-free'
|
||||
bookingInfo = `<p>This is not the correct page to pay for TestDrive.</p>`
|
||||
header = `<div><h2>enter card info</h2>
|
||||
|
||||
<div className="no-charge">Your card wil not be charged.<br/>See explanation to the right.</div>
|
||||
</div>`
|
||||
bookingInfo = `<p>You are booking a single free {this.state.lesson.lesson_length}-minute lesson.</p>`
|
||||
bookingDetail = `<p>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.
|
||||
<br/>
|
||||
|
||||
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></div>
|
||||
</p>`
|
||||
else if lesson.lesson_type == 'test-drive'
|
||||
bookingInfo = `<p>This is not the correct page to pay for TestDrive.</p>`
|
||||
header = `<div><h2>enter payment info for test drive</h2></div>`
|
||||
bookingInfo = `<p></p>`
|
||||
bookingDetail = `<p>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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></span>
|
||||
</p>`
|
||||
else if lesson.lesson_type == 'paid'
|
||||
bookingInfo = `<p>You are booking a {this.state.lesson.lesson_length} minute lesson for ${this.state.lesson.booked_price.toFixed(2)}</p>`
|
||||
header = `<div><h2>enter payment info for lesson</h2></div>`
|
||||
if this.state.lesson.recurring
|
||||
if this.state.lesson.payment_style == 'single'
|
||||
bookingInfo = `<p>You are booking a {this.state.lesson.lesson_length} minute lesson for
|
||||
${this.state.lesson.booked_price.toFixed(2)}</p>`
|
||||
bookingDetail = `<p>
|
||||
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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></span>
|
||||
</p>`
|
||||
else if this.state.lesson.payment_style == 'weekly'
|
||||
bookingInfo = `<p>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.</p>`
|
||||
bookingDetail = `<p>
|
||||
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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></span>
|
||||
</p>`
|
||||
else if this.state.lesson.payment_style == 'monthly'
|
||||
bookingInfo = `<p>You are booking a weekly recurring series of {this.state.lesson.lesson_length}-minute
|
||||
lessons, to be paid for monthly until cancelled.</p>`
|
||||
bookingDetail = `<p>
|
||||
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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></span>
|
||||
</p>`
|
||||
else
|
||||
bookingInfo = `<p>You are booking a {this.state.lesson.lesson_length} minute lesson for
|
||||
${this.state.lesson.booked_price.toFixed(2)}</p>`
|
||||
bookingDetail = `<p>
|
||||
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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></span>
|
||||
</p>`
|
||||
else
|
||||
header = `<div><h2>enter payment info</h2></div>`
|
||||
bookingInfo = `<p>You are entering your credit card info so that later checkouts go quickly. You can skip this
|
||||
for now.</p>`
|
||||
bookingDetail = `
|
||||
<p>
|
||||
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.
|
||||
<br/>
|
||||
|
||||
<span className="jamclass-policies plain"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass policies</a></span>
|
||||
</p>`
|
||||
|
||||
|
||||
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}
|
||||
|
||||
`<div className="content-body-scroller">
|
||||
<div className="column column-left">
|
||||
<h2>enter payment info for lesson</h2>
|
||||
{header}
|
||||
|
||||
<div className="field card-number">
|
||||
<label>Card Number:</label>
|
||||
<input type="text" name="card-number"></input>
|
||||
</div>
|
||||
<div className="field expiration">
|
||||
<label>Expiration Date:</label>
|
||||
<input type="text" name="expiration"></input>
|
||||
</div>
|
||||
<div className="field cvv">
|
||||
<label>CVV:</label>
|
||||
<input type="text" name="cvv"></input>
|
||||
</div>
|
||||
|
||||
<div className="field zip">
|
||||
<label>Zip/Postal Code:</label>
|
||||
<input type="text" name="zip"></input>
|
||||
</div>
|
||||
<form autocomplete="on" onSubmit={this.onSubmit}>
|
||||
<div className={classNames(cardNumberFieldClasses)}>
|
||||
<label>Card Number:</label>
|
||||
<input placeholder="1234 5678 9123 4567" type="tel" autocomplete="cc-number" disabled={disabled}
|
||||
type="text" name="card-number" className="card-number"></input>
|
||||
</div>
|
||||
<div className={classNames(expirationFieldClasses)}>
|
||||
<label>Expiration Date:</label>
|
||||
<input placeholder="MM / YY" autocomplete="cc-expiry" disabled={disabled} type="text" name="expiration"
|
||||
className="expiration"></input>
|
||||
</div>
|
||||
<div className={classNames(cvvFieldClasses)}>
|
||||
<label>CVV:</label>
|
||||
<input autocomplete="off" disabled={disabled} type="text" name="cvv" className="cvv"></input>
|
||||
</div>
|
||||
<div className={classNames(zipCodeClasses)}>
|
||||
<label>Zip Code</label>
|
||||
<input autocomplete="off" disabled={disabled || !this.state.billingInUS} type="text" name="zip"
|
||||
className="zip"></input>
|
||||
</div>
|
||||
<div className={classNames(inUSClasses)}>
|
||||
<label>Billing Address<br/>is in the U.S.</label>
|
||||
<input type="checkbox" name="billing-address-in-us" className="billing-address-in-us"
|
||||
value={this.state.billingInUS}/>
|
||||
</div>
|
||||
<input style={{'display':'none'}} type="submit" name="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="column column-right">
|
||||
{teacherDetails}
|
||||
<div className="booking-info">
|
||||
<p>{bookingInfo}</p>
|
||||
{bookingInfo}
|
||||
|
||||
<p>BOOKING DETAIL TODO<br/>
|
||||
|
||||
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
||||
policies</a></div>
|
||||
</p>
|
||||
{bookingDetail}
|
||||
</div>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
|
||||
<div className="actions">
|
||||
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
|
||||
className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>`
|
||||
|
||||
})
|
||||
|
|
@ -3,5 +3,6 @@ context = window
|
|||
@UserActions = Reflux.createActions({
|
||||
loaded: {}
|
||||
modify: {}
|
||||
refresh: {}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@
|
|||
tbody {
|
||||
|
||||
}
|
||||
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
object @jamblasters
|
||||
|
||||
|
||||
attributes :id, :serial_no, :client_id, :vtoken
|
||||
attributes :id, :serial_no, :client_id, :vtoken, :key
|
||||
|
|
@ -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
|
||||
}
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
object @lesson
|
||||
|
||||
node :lesson do |lesson|
|
||||
attribute :id
|
||||
end
|
||||
|
||||
if @test_drive
|
||||
node :test_drive do |lesson|
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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" %>
|
||||
|
|
|
|||
|
|
@ -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', {}
|
||||
|
||||
|
|
@ -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', {}
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
object @exception
|
||||
|
||||
node do |exception|
|
||||
errors = {}
|
||||
errors["message"] = [exception.to_s]
|
||||
{
|
||||
errors: errors
|
||||
}
|
||||
end
|
||||
|
||||
node "type" do
|
||||
"StripeError"
|
||||
end
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
<%= yield %>
|
||||
<%= render "shared/ga" %>
|
||||
<%= render "shared/recurly" %>
|
||||
<%= render "shared/stripe" %>
|
||||
<%= render "shared/google_nocaptcha" %>
|
||||
<%= render "shared/olark" %>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
javascript:
|
||||
window.stripeReadyHandler = function() {
|
||||
Stripe.setPublishableKey(gon.stripe_publishable_key);
|
||||
}
|
||||
|
||||
script src="https://js.stripe.com/v2/" onload="window.stripeReadyHandler()" async
|
||||
|
|
@ -164,6 +164,9 @@ if defined?(Bundler)
|
|||
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
|
||||
config.recurly_public_api_key = 'sjc-SZlO11shkeA1WMGuISLGg5'
|
||||
|
||||
config.stripe_secret_key = 'sk_test_cPVRbtr9xbMiqffV8jwibwLA'
|
||||
config.stripe_publishable_key = 'pk_test_9vO8ZnxBpb9Udb0paruV3qLv'
|
||||
|
||||
if Rails.env == 'production'
|
||||
config.desk_url = 'https://jamkazam.desk.com'
|
||||
config.multipass_callback_url = "http://jamkazam.desk.com/customer/authentication/multipass/callback"
|
||||
|
|
@ -418,5 +421,11 @@ if defined?(Bundler)
|
|||
config.ban_jamtrack_downloaders = true
|
||||
config.chat_opened_by_default = true
|
||||
config.chat_blast = true
|
||||
end
|
||||
|
||||
config.stripe = {
|
||||
:publishable_key => 'pk_test_9vO8ZnxBpb9Udb0paruV3qLv',
|
||||
:secret_key => 'sk_test_cPVRbtr9xbMiqffV8jwibwLA'
|
||||
}
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Stripe.api_key = Rails.configuration.stripe[:secret_key]
|
||||
|
|
@ -0,0 +1 @@
|
|||
ZipCodes.load if Rails.env.production?
|
||||
|
|
@ -684,7 +684,10 @@ SampleApp::Application.routes.draw do
|
|||
match '/jamblasters/pairing/store' => 'api_jamblasters#store_token', :via => :post
|
||||
match '/jamblasters/pairing/pair' => 'api_jamblasters#pair', :via => :post
|
||||
|
||||
match '/lesson_sessions' => 'api_lesson_sessions#index', :via => :get
|
||||
match '/lesson_bookings' => 'api_lesson_bookings#create', :via => :post
|
||||
match '/lesson_booking/unprocessed' => 'api_lesson_bookings#unprocessed', :via => :get
|
||||
|
||||
match '/stripe' => 'api_stripe#store', :via => :post
|
||||
end
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,651 @@
|
|||
// Generated by CoffeeScript 1.7.1
|
||||
(function() {
|
||||
var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,
|
||||
__slice = [].slice,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
$ = window.jQuery || window.Zepto || window.$;
|
||||
|
||||
$.payment = {};
|
||||
|
||||
$.payment.fn = {};
|
||||
|
||||
$.fn.payment = function() {
|
||||
var args, method;
|
||||
method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
|
||||
return $.payment.fn[method].apply(this, args);
|
||||
};
|
||||
|
||||
defaultFormat = /(\d{1,4})/g;
|
||||
|
||||
$.payment.cards = cards = [
|
||||
{
|
||||
type: 'visaelectron',
|
||||
patterns: [4026, 417500, 4405, 4508, 4844, 4913, 4917],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'maestro',
|
||||
patterns: [5018, 502, 503, 56, 58, 639, 6220, 67],
|
||||
format: defaultFormat,
|
||||
length: [12, 13, 14, 15, 16, 17, 18, 19],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'forbrugsforeningen',
|
||||
patterns: [600],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'dankort',
|
||||
patterns: [5019],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'visa',
|
||||
patterns: [4],
|
||||
format: defaultFormat,
|
||||
length: [13, 16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'mastercard',
|
||||
patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'amex',
|
||||
patterns: [34, 37],
|
||||
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
|
||||
length: [15],
|
||||
cvcLength: [3, 4],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'dinersclub',
|
||||
patterns: [30, 36, 38, 39],
|
||||
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
|
||||
length: [14],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'discover',
|
||||
patterns: [60, 64, 65, 622],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}, {
|
||||
type: 'unionpay',
|
||||
patterns: [62, 88],
|
||||
format: defaultFormat,
|
||||
length: [16, 17, 18, 19],
|
||||
cvcLength: [3],
|
||||
luhn: false
|
||||
}, {
|
||||
type: 'jcb',
|
||||
patterns: [35],
|
||||
format: defaultFormat,
|
||||
length: [16],
|
||||
cvcLength: [3],
|
||||
luhn: true
|
||||
}
|
||||
];
|
||||
|
||||
cardFromNumber = function(num) {
|
||||
var card, p, pattern, _i, _j, _len, _len1, _ref;
|
||||
num = (num + '').replace(/\D/g, '');
|
||||
for (_i = 0, _len = cards.length; _i < _len; _i++) {
|
||||
card = cards[_i];
|
||||
_ref = card.patterns;
|
||||
for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
|
||||
pattern = _ref[_j];
|
||||
p = pattern + '';
|
||||
if (num.substr(0, p.length) === p) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cardFromType = function(type) {
|
||||
var card, _i, _len;
|
||||
for (_i = 0, _len = cards.length; _i < _len; _i++) {
|
||||
card = cards[_i];
|
||||
if (card.type === type) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
luhnCheck = function(num) {
|
||||
var digit, digits, odd, sum, _i, _len;
|
||||
odd = true;
|
||||
sum = 0;
|
||||
digits = (num + '').split('').reverse();
|
||||
for (_i = 0, _len = digits.length; _i < _len; _i++) {
|
||||
digit = digits[_i];
|
||||
digit = parseInt(digit, 10);
|
||||
if ((odd = !odd)) {
|
||||
digit *= 2;
|
||||
}
|
||||
if (digit > 9) {
|
||||
digit -= 9;
|
||||
}
|
||||
sum += digit;
|
||||
}
|
||||
return sum % 10 === 0;
|
||||
};
|
||||
|
||||
hasTextSelected = function($target) {
|
||||
var _ref;
|
||||
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
|
||||
return true;
|
||||
}
|
||||
if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {
|
||||
if (document.selection.createRange().text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
safeVal = function(value, $target) {
|
||||
var cursor, error, last;
|
||||
try {
|
||||
cursor = $target.prop('selectionStart');
|
||||
} catch (_error) {
|
||||
error = _error;
|
||||
cursor = null;
|
||||
}
|
||||
last = $target.val();
|
||||
$target.val(value);
|
||||
if (cursor !== null && $target.is(":focus")) {
|
||||
if (cursor === last.length) {
|
||||
cursor = value.length;
|
||||
}
|
||||
$target.prop('selectionStart', cursor);
|
||||
return $target.prop('selectionEnd', cursor);
|
||||
}
|
||||
};
|
||||
|
||||
replaceFullWidthChars = function(str) {
|
||||
var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;
|
||||
if (str == null) {
|
||||
str = '';
|
||||
}
|
||||
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
|
||||
halfWidth = '0123456789';
|
||||
value = '';
|
||||
chars = str.split('');
|
||||
for (_i = 0, _len = chars.length; _i < _len; _i++) {
|
||||
chr = chars[_i];
|
||||
idx = fullWidth.indexOf(chr);
|
||||
if (idx > -1) {
|
||||
chr = halfWidth[idx];
|
||||
}
|
||||
value += chr;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
reFormatNumeric = function(e) {
|
||||
var $target;
|
||||
$target = $(e.currentTarget);
|
||||
return setTimeout(function() {
|
||||
var value;
|
||||
value = $target.val();
|
||||
value = replaceFullWidthChars(value);
|
||||
value = value.replace(/\D/g, '');
|
||||
return safeVal(value, $target);
|
||||
});
|
||||
};
|
||||
|
||||
reFormatCardNumber = function(e) {
|
||||
var $target;
|
||||
$target = $(e.currentTarget);
|
||||
return setTimeout(function() {
|
||||
var value;
|
||||
value = $target.val();
|
||||
value = replaceFullWidthChars(value);
|
||||
value = $.payment.formatCardNumber(value);
|
||||
return safeVal(value, $target);
|
||||
});
|
||||
};
|
||||
|
||||
formatCardNumber = function(e) {
|
||||
var $target, card, digit, length, re, upperLength, value;
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
$target = $(e.currentTarget);
|
||||
value = $target.val();
|
||||
card = cardFromNumber(value + digit);
|
||||
length = (value.replace(/\D/g, '') + digit).length;
|
||||
upperLength = 16;
|
||||
if (card) {
|
||||
upperLength = card.length[card.length.length - 1];
|
||||
}
|
||||
if (length >= upperLength) {
|
||||
return;
|
||||
}
|
||||
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
|
||||
return;
|
||||
}
|
||||
if (card && card.type === 'amex') {
|
||||
re = /^(\d{4}|\d{4}\s\d{6})$/;
|
||||
} else {
|
||||
re = /(?:^|\s)(\d{4})$/;
|
||||
}
|
||||
if (re.test(value)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val(value + ' ' + digit);
|
||||
});
|
||||
} else if (re.test(value + digit)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val(value + digit + ' ');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
formatBackCardNumber = function(e) {
|
||||
var $target, value;
|
||||
$target = $(e.currentTarget);
|
||||
value = $target.val();
|
||||
if (e.which !== 8) {
|
||||
return;
|
||||
}
|
||||
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
|
||||
return;
|
||||
}
|
||||
if (/\d\s$/.test(value)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val(value.replace(/\d\s$/, ''));
|
||||
});
|
||||
} else if (/\s\d?$/.test(value)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val(value.replace(/\d$/, ''));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reFormatExpiry = function(e) {
|
||||
var $target;
|
||||
$target = $(e.currentTarget);
|
||||
return setTimeout(function() {
|
||||
var value;
|
||||
value = $target.val();
|
||||
value = replaceFullWidthChars(value);
|
||||
value = $.payment.formatExpiry(value);
|
||||
return safeVal(value, $target);
|
||||
});
|
||||
};
|
||||
|
||||
formatExpiry = function(e) {
|
||||
var $target, digit, val;
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
$target = $(e.currentTarget);
|
||||
val = $target.val() + digit;
|
||||
if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val("0" + val + " / ");
|
||||
});
|
||||
} else if (/^\d\d$/.test(val)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
var m1, m2;
|
||||
m1 = parseInt(val[0], 10);
|
||||
m2 = parseInt(val[1], 10);
|
||||
if (m2 > 2 && m1 !== 0) {
|
||||
return $target.val("0" + m1 + " / " + m2);
|
||||
} else {
|
||||
return $target.val("" + val + " / ");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
formatForwardExpiry = function(e) {
|
||||
var $target, digit, val;
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
$target = $(e.currentTarget);
|
||||
val = $target.val();
|
||||
if (/^\d\d$/.test(val)) {
|
||||
return $target.val("" + val + " / ");
|
||||
}
|
||||
};
|
||||
|
||||
formatForwardSlashAndSpace = function(e) {
|
||||
var $target, val, which;
|
||||
which = String.fromCharCode(e.which);
|
||||
if (!(which === '/' || which === ' ')) {
|
||||
return;
|
||||
}
|
||||
$target = $(e.currentTarget);
|
||||
val = $target.val();
|
||||
if (/^\d$/.test(val) && val !== '0') {
|
||||
return $target.val("0" + val + " / ");
|
||||
}
|
||||
};
|
||||
|
||||
formatBackExpiry = function(e) {
|
||||
var $target, value;
|
||||
$target = $(e.currentTarget);
|
||||
value = $target.val();
|
||||
if (e.which !== 8) {
|
||||
return;
|
||||
}
|
||||
if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
|
||||
return;
|
||||
}
|
||||
if (/\d\s\/\s$/.test(value)) {
|
||||
e.preventDefault();
|
||||
return setTimeout(function() {
|
||||
return $target.val(value.replace(/\d\s\/\s$/, ''));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reFormatCVC = function(e) {
|
||||
var $target;
|
||||
$target = $(e.currentTarget);
|
||||
return setTimeout(function() {
|
||||
var value;
|
||||
value = $target.val();
|
||||
value = replaceFullWidthChars(value);
|
||||
value = value.replace(/\D/g, '').slice(0, 4);
|
||||
return safeVal(value, $target);
|
||||
});
|
||||
};
|
||||
|
||||
restrictNumeric = function(e) {
|
||||
var input;
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return true;
|
||||
}
|
||||
if (e.which === 32) {
|
||||
return false;
|
||||
}
|
||||
if (e.which === 0) {
|
||||
return true;
|
||||
}
|
||||
if (e.which < 33) {
|
||||
return true;
|
||||
}
|
||||
input = String.fromCharCode(e.which);
|
||||
return !!/[\d\s]/.test(input);
|
||||
};
|
||||
|
||||
restrictCardNumber = function(e) {
|
||||
var $target, card, digit, value;
|
||||
$target = $(e.currentTarget);
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
if (hasTextSelected($target)) {
|
||||
return;
|
||||
}
|
||||
value = ($target.val() + digit).replace(/\D/g, '');
|
||||
card = cardFromNumber(value);
|
||||
if (card) {
|
||||
return value.length <= card.length[card.length.length - 1];
|
||||
} else {
|
||||
return value.length <= 16;
|
||||
}
|
||||
};
|
||||
|
||||
restrictExpiry = function(e) {
|
||||
var $target, digit, value;
|
||||
$target = $(e.currentTarget);
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
if (hasTextSelected($target)) {
|
||||
return;
|
||||
}
|
||||
value = $target.val() + digit;
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value.length > 6) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
restrictCVC = function(e) {
|
||||
var $target, digit, val;
|
||||
$target = $(e.currentTarget);
|
||||
digit = String.fromCharCode(e.which);
|
||||
if (!/^\d+$/.test(digit)) {
|
||||
return;
|
||||
}
|
||||
if (hasTextSelected($target)) {
|
||||
return;
|
||||
}
|
||||
val = $target.val() + digit;
|
||||
return val.length <= 4;
|
||||
};
|
||||
|
||||
setCardType = function(e) {
|
||||
var $target, allTypes, card, cardType, val;
|
||||
$target = $(e.currentTarget);
|
||||
val = $target.val();
|
||||
cardType = $.payment.cardType(val) || 'unknown';
|
||||
if (!$target.hasClass(cardType)) {
|
||||
allTypes = (function() {
|
||||
var _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = cards.length; _i < _len; _i++) {
|
||||
card = cards[_i];
|
||||
_results.push(card.type);
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
$target.removeClass('unknown');
|
||||
$target.removeClass(allTypes.join(' '));
|
||||
$target.addClass(cardType);
|
||||
$target.toggleClass('identified', cardType !== 'unknown');
|
||||
return $target.trigger('payment.cardType', cardType);
|
||||
}
|
||||
};
|
||||
|
||||
$.payment.fn.formatCardCVC = function() {
|
||||
this.on('keypress', restrictNumeric);
|
||||
this.on('keypress', restrictCVC);
|
||||
this.on('paste', reFormatCVC);
|
||||
this.on('change', reFormatCVC);
|
||||
this.on('input', reFormatCVC);
|
||||
return this;
|
||||
};
|
||||
|
||||
$.payment.fn.formatCardExpiry = function() {
|
||||
this.on('keypress', restrictNumeric);
|
||||
this.on('keypress', restrictExpiry);
|
||||
this.on('keypress', formatExpiry);
|
||||
this.on('keypress', formatForwardSlashAndSpace);
|
||||
this.on('keypress', formatForwardExpiry);
|
||||
this.on('keydown', formatBackExpiry);
|
||||
this.on('change', reFormatExpiry);
|
||||
this.on('input', reFormatExpiry);
|
||||
return this;
|
||||
};
|
||||
|
||||
$.payment.fn.formatCardNumber = function() {
|
||||
this.on('keypress', restrictNumeric);
|
||||
this.on('keypress', restrictCardNumber);
|
||||
this.on('keypress', formatCardNumber);
|
||||
this.on('keydown', formatBackCardNumber);
|
||||
this.on('keyup', setCardType);
|
||||
this.on('paste', reFormatCardNumber);
|
||||
this.on('change', reFormatCardNumber);
|
||||
this.on('input', reFormatCardNumber);
|
||||
this.on('input', setCardType);
|
||||
return this;
|
||||
};
|
||||
|
||||
$.payment.fn.restrictNumeric = function() {
|
||||
this.on('keypress', restrictNumeric);
|
||||
this.on('paste', reFormatNumeric);
|
||||
this.on('change', reFormatNumeric);
|
||||
this.on('input', reFormatNumeric);
|
||||
return this;
|
||||
};
|
||||
|
||||
$.payment.fn.cardExpiryVal = function() {
|
||||
return $.payment.cardExpiryVal($(this).val());
|
||||
};
|
||||
|
||||
$.payment.cardExpiryVal = function(value) {
|
||||
var month, prefix, year, _ref;
|
||||
_ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1];
|
||||
if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
|
||||
prefix = (new Date).getFullYear();
|
||||
prefix = prefix.toString().slice(0, 2);
|
||||
year = prefix + year;
|
||||
}
|
||||
month = parseInt(month, 10);
|
||||
year = parseInt(year, 10);
|
||||
return {
|
||||
month: month,
|
||||
year: year
|
||||
};
|
||||
};
|
||||
|
||||
$.payment.validateCardNumber = function(num) {
|
||||
var card, _ref;
|
||||
num = (num + '').replace(/\s+|-/g, '');
|
||||
if (!/^\d+$/.test(num)) {
|
||||
return false;
|
||||
}
|
||||
card = cardFromNumber(num);
|
||||
if (!card) {
|
||||
return false;
|
||||
}
|
||||
return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
|
||||
};
|
||||
|
||||
$.payment.validateCardExpiry = function(month, year) {
|
||||
var currentTime, expiry, _ref;
|
||||
if (typeof month === 'object' && 'month' in month) {
|
||||
_ref = month, month = _ref.month, year = _ref.year;
|
||||
}
|
||||
if (!(month && year)) {
|
||||
return false;
|
||||
}
|
||||
month = $.trim(month);
|
||||
year = $.trim(year);
|
||||
if (!/^\d+$/.test(month)) {
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+$/.test(year)) {
|
||||
return false;
|
||||
}
|
||||
if (!((1 <= month && month <= 12))) {
|
||||
return false;
|
||||
}
|
||||
if (year.length === 2) {
|
||||
if (year < 70) {
|
||||
year = "20" + year;
|
||||
} else {
|
||||
year = "19" + year;
|
||||
}
|
||||
}
|
||||
if (year.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
expiry = new Date(year, month);
|
||||
currentTime = new Date;
|
||||
expiry.setMonth(expiry.getMonth() - 1);
|
||||
expiry.setMonth(expiry.getMonth() + 1, 1);
|
||||
return expiry > currentTime;
|
||||
};
|
||||
|
||||
$.payment.validateCardCVC = function(cvc, type) {
|
||||
var card, _ref;
|
||||
cvc = $.trim(cvc);
|
||||
if (!/^\d+$/.test(cvc)) {
|
||||
return false;
|
||||
}
|
||||
card = cardFromType(type);
|
||||
if (card != null) {
|
||||
return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;
|
||||
} else {
|
||||
return cvc.length >= 3 && cvc.length <= 4;
|
||||
}
|
||||
};
|
||||
|
||||
$.payment.cardType = function(num) {
|
||||
var _ref;
|
||||
if (!num) {
|
||||
return null;
|
||||
}
|
||||
return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
|
||||
};
|
||||
|
||||
$.payment.formatCardNumber = function(num) {
|
||||
var card, groups, upperLength, _ref;
|
||||
num = num.replace(/\D/g, '');
|
||||
card = cardFromNumber(num);
|
||||
if (!card) {
|
||||
return num;
|
||||
}
|
||||
upperLength = card.length[card.length.length - 1];
|
||||
num = num.slice(0, upperLength);
|
||||
if (card.format.global) {
|
||||
return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
|
||||
} else {
|
||||
groups = card.format.exec(num);
|
||||
if (groups == null) {
|
||||
return;
|
||||
}
|
||||
groups.shift();
|
||||
groups = $.grep(groups, function(n) {
|
||||
return n;
|
||||
});
|
||||
return groups.join(' ');
|
||||
}
|
||||
};
|
||||
|
||||
$.payment.formatExpiry = function(expiry) {
|
||||
var mon, parts, sep, year;
|
||||
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
|
||||
if (!parts) {
|
||||
return '';
|
||||
}
|
||||
mon = parts[1] || '';
|
||||
sep = parts[2] || '';
|
||||
year = parts[3] || '';
|
||||
if (year.length > 0) {
|
||||
sep = ' / ';
|
||||
} else if (sep === ' /') {
|
||||
mon = mon.substring(0, 1);
|
||||
sep = '';
|
||||
} else if (mon.length === 2 || sep.length > 0) {
|
||||
sep = ' / ';
|
||||
} else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
|
||||
mon = "0" + mon;
|
||||
sep = ' / ';
|
||||
}
|
||||
return mon + sep + year;
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
|
|
@ -1310,7 +1310,6 @@ module JamWebsockets
|
|||
@message_stats['total_time'] = total_time
|
||||
@message_stats['banned_users'] = @temp_ban.length
|
||||
|
||||
|
||||
Stats.write('gateway.stats', @message_stats)
|
||||
|
||||
# clear out stats
|
||||
|
|
|
|||
Loading…
Reference in New Issue