VRFS-3965 - manage payment features for students
This commit is contained in:
parent
15d968dff5
commit
309ebb4e5a
|
|
@ -345,4 +345,5 @@ lessons.sql
|
|||
lessons_unread_messages.sql
|
||||
track_school_signups.sql
|
||||
add_test_drive_types.sql
|
||||
updated_subjects.sql
|
||||
updated_subjects.sql
|
||||
update_payment_history.sql
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE charges ADD COLUMN user_id VARCHAR(64) REFERENCES users(id) NOT NULL;
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
module JamRuby
|
||||
class Charge < ActiveRecord::Base
|
||||
|
||||
belongs_to :user, class_name: "JamRuby::User"
|
||||
|
||||
validates :sent_billing_notices, inclusion: {in: [true, false]}
|
||||
|
||||
def max_retries
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ module JamRuby
|
|||
validate :validate_lesson_booking_slots
|
||||
validate :validate_lesson_length
|
||||
validate :validate_payment_style
|
||||
validate :validate_uncollectables
|
||||
validate :validate_accepted, :if => :accepting
|
||||
validate :validate_canceled, :if => :canceling
|
||||
|
||||
|
||||
|
||||
before_save :before_save
|
||||
before_validation :before_validation
|
||||
after_create :after_create
|
||||
|
|
@ -603,7 +603,7 @@ module JamRuby
|
|||
#end
|
||||
elsif is_test_drive?
|
||||
if user.has_requested_test_drive?(teacher) && !user.admin
|
||||
errors.add(:user, "has a requested TestDrive with this teacher")
|
||||
errors.add(:user, "have a requested TestDrive with this teacher")
|
||||
end
|
||||
if !user.has_test_drives? && !user.can_buy_test_drive?
|
||||
errors.add(:user, "have no remaining test drives")
|
||||
|
|
@ -651,6 +651,11 @@ module JamRuby
|
|||
end
|
||||
end
|
||||
|
||||
def validate_uncollectables
|
||||
if user.uncollectables.count > 0
|
||||
errors.add(:user, 'have unpaid lessons.')
|
||||
end
|
||||
end
|
||||
|
||||
def self.book_free(user, teacher, lesson_booking_slots, description)
|
||||
self.book(user, teacher, LessonBooking::LESSON_TYPE_FREE, lesson_booking_slots, false, 30, PAYMENT_STYLE_ELSEWHERE, description)
|
||||
|
|
|
|||
|
|
@ -33,11 +33,14 @@ module JamRuby
|
|||
end
|
||||
|
||||
def create_charge
|
||||
self.lesson_payment_charge = LessonPaymentCharge.new
|
||||
lesson_payment_charge.amount_in_cents = 0
|
||||
lesson_payment_charge.fee_in_cents = 0
|
||||
lesson_payment_charge.lesson_package_purchase = self
|
||||
lesson_payment_charge.save!
|
||||
if self.lesson_booking.is_monthly_payment?
|
||||
self.lesson_payment_charge = LessonPaymentCharge.new
|
||||
lesson_payment_charge.user = user
|
||||
lesson_payment_charge.amount_in_cents = 0
|
||||
lesson_payment_charge.fee_in_cents = 0
|
||||
lesson_payment_charge.lesson_package_purchase = self
|
||||
lesson_payment_charge.save!
|
||||
end
|
||||
end
|
||||
|
||||
def add_test_drives
|
||||
|
|
@ -94,14 +97,17 @@ module JamRuby
|
|||
(price * 100).to_i
|
||||
end
|
||||
|
||||
def description(lesson_booking)
|
||||
lesson_package_type.description(lesson_booking)
|
||||
def description(lesson_booking, time = false)
|
||||
lesson_package_type.description(lesson_booking, time)
|
||||
end
|
||||
|
||||
def stripe_description(lesson_booking)
|
||||
description(lesson_booking)
|
||||
end
|
||||
|
||||
def timed_description
|
||||
"Lessons for the month of #{self.month_name} with #{self.lesson_booking.student.name}"
|
||||
end
|
||||
|
||||
def month_name
|
||||
if recurring
|
||||
|
|
@ -116,6 +122,7 @@ module JamRuby
|
|||
end
|
||||
|
||||
|
||||
|
||||
def bill_monthly(force = false)
|
||||
lesson_payment_charge.charge(force)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module JamRuby
|
|||
end
|
||||
|
||||
def charged_user
|
||||
@charged_user ||= target.student
|
||||
user
|
||||
end
|
||||
|
||||
def resolve_target
|
||||
|
|
@ -31,6 +31,10 @@ module JamRuby
|
|||
charged_user
|
||||
end
|
||||
|
||||
def teacher
|
||||
target.teacher
|
||||
end
|
||||
|
||||
def is_lesson?
|
||||
!lesson_session.nil?
|
||||
end
|
||||
|
|
@ -85,5 +89,13 @@ module JamRuby
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def description
|
||||
target.timed_description
|
||||
end
|
||||
|
||||
def expected_price_in_cents
|
||||
target.lesson_booking.distribution_price_in_cents(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,13 +5,13 @@ module JamRuby
|
|||
include HtmlSanitize
|
||||
html_sanitize strict: [:cancel_message]
|
||||
|
||||
attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling
|
||||
attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson, :canceling, :assigned_student
|
||||
|
||||
|
||||
@@log = Logging.logger[LessonSession]
|
||||
|
||||
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true
|
||||
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, to: :lesson_booking
|
||||
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, to: :lesson_booking
|
||||
delegate :pretty_scheduled_start, to: :music_session
|
||||
|
||||
|
||||
|
|
@ -75,8 +75,9 @@ module JamRuby
|
|||
scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now) }
|
||||
|
||||
def create_charge
|
||||
if !is_test_drive?
|
||||
if !is_test_drive? && !is_monthly_payment?
|
||||
self.lesson_payment_charge = LessonPaymentCharge.new
|
||||
lesson_payment_charge.user = @assigned_student
|
||||
lesson_payment_charge.amount_in_cents = 0
|
||||
lesson_payment_charge.fee_in_cents = 0
|
||||
lesson_payment_charge.lesson_session = self
|
||||
|
|
@ -432,6 +433,7 @@ module JamRuby
|
|||
lesson_session.teacher = booking.teacher
|
||||
lesson_session.status = booking.status
|
||||
lesson_session.slot = booking.default_slot
|
||||
lesson_session.assigned_student = booking.student
|
||||
if booking.is_test_drive?
|
||||
lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase
|
||||
end
|
||||
|
|
@ -711,6 +713,19 @@ module JamRuby
|
|||
lesson_booking.lesson_package_type.description(lesson_booking)
|
||||
end
|
||||
|
||||
def timed_description
|
||||
if is_test_drive?
|
||||
"TestDrive session with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
||||
else
|
||||
if self.lesson_booking.is_monthly_payment?
|
||||
"Monthly Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
||||
else
|
||||
"Lesson with #{self.lesson_booking.student.name} on #{self.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def stripe_description(lesson_booking)
|
||||
description(lesson_booking)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ module JamRuby
|
|||
|
||||
belongs_to :sale
|
||||
belongs_to :recurly_transaction_web_hook
|
||||
belongs_to :charge
|
||||
|
||||
|
||||
def self.index(user, params = {})
|
||||
|
|
@ -14,7 +15,7 @@ module JamRuby
|
|||
limit = limit.to_i
|
||||
|
||||
query = PaymentHistory.limit(limit)
|
||||
.includes(sale: [:sale_line_items], recurly_transaction_web_hook:[])
|
||||
.includes(sale: [:sale_line_items], recurly_transaction_web_hook:[], charge:[])
|
||||
.where(user_id: user.id)
|
||||
.where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'")
|
||||
.order('created_at DESC')
|
||||
|
|
|
|||
|
|
@ -104,17 +104,9 @@ module JamRuby
|
|||
|
||||
def description
|
||||
if lesson_session
|
||||
if lesson_session.lesson_booking.is_test_drive?
|
||||
"TestDrive session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
||||
elsif lesson_session.lesson_booking.is_normal?
|
||||
if lesson_session.lesson_booking.is_weekly_payment? || lesson_session.lesson_booking.is_monthly_payment?
|
||||
raise "Should not be here"
|
||||
else
|
||||
"Lesson with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date.strftime('%B %d, %Y')}"
|
||||
end
|
||||
end
|
||||
lesson_session.timed_description
|
||||
else
|
||||
"Lessons for the month of #{lesson_package_purchase.month_name} with #{lesson_package_purchase.lesson_booking.student.name}"
|
||||
lesson_package_purchase.description
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ module JamRuby
|
|||
|
||||
if payment.teacher_payment_charge.nil?
|
||||
charge = TeacherPaymentCharge.new
|
||||
charge.user = teacher
|
||||
charge.amount_in_cents = payment.amount_in_cents
|
||||
charge.fee_in_cents = payment.fee_in_cents
|
||||
charge.teacher_payment = payment
|
||||
|
|
|
|||
|
|
@ -1957,6 +1957,7 @@ module JamRuby
|
|||
def card_approved(token, zip, booking_id)
|
||||
|
||||
approved_booking = nil
|
||||
found_uncollectables = nil
|
||||
User.transaction do
|
||||
self.stripe_token = token if token
|
||||
self.stripe_zip_code = zip if zip
|
||||
|
|
@ -1970,9 +1971,16 @@ module JamRuby
|
|||
approved_booking.card_approved
|
||||
end
|
||||
end
|
||||
|
||||
if uncollectables.count > 0
|
||||
found_uncollectables = uncollectables
|
||||
uncollectables.update_all(billing_should_retry: true)
|
||||
else
|
||||
found_uncollectables = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
approved_booking
|
||||
[approved_booking, found_uncollectables]
|
||||
end
|
||||
|
||||
def update_name(name)
|
||||
|
|
@ -2002,6 +2010,7 @@ module JamRuby
|
|||
intent = nil
|
||||
purchase = nil
|
||||
lesson_package_type = nil
|
||||
uncollectables = nil
|
||||
User.transaction do
|
||||
|
||||
if params[:name].present?
|
||||
|
|
@ -2010,7 +2019,7 @@ module JamRuby
|
|||
end
|
||||
end
|
||||
|
||||
booking = card_approved(params[:token], params[:zip], params[:booking_id])
|
||||
booking, uncollectables = card_approved(params[:token], params[:zip], params[:booking_id])
|
||||
if params[:test_drive]
|
||||
self.reload
|
||||
if booking
|
||||
|
|
@ -2035,7 +2044,7 @@ module JamRuby
|
|||
|
||||
end
|
||||
|
||||
{lesson: booking, test_drive: test_drive, purchase: purchase, lesson_package_type: lesson_package_type}
|
||||
{lesson: booking, test_drive: test_drive, purchase: purchase, lesson_package_type: lesson_package_type, uncollectables: uncollectables}
|
||||
end
|
||||
|
||||
def requested_test_drive(teacher = nil)
|
||||
|
|
@ -2095,6 +2104,10 @@ module JamRuby
|
|||
total_test_drives - remaining_test_drives
|
||||
end
|
||||
|
||||
def uncollectables(limit = 10)
|
||||
LessonPaymentCharge.where(user_id:self.id).order(:created_at).where('billing_attempts > 0').where(billed: false).limit(limit)
|
||||
end
|
||||
|
||||
def has_rated_teacher(teacher)
|
||||
if teacher.is_a?(JamRuby::User)
|
||||
teacher = teacher.teacher
|
||||
|
|
|
|||
|
|
@ -1010,6 +1010,13 @@ FactoryGirl.define do
|
|||
|
||||
factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do
|
||||
type 'JamRuby::TeacherPaymentCharge'
|
||||
association :user, factory: :user
|
||||
end
|
||||
|
||||
|
||||
factory :lesson_payment_charge, parent: :charge, class: 'JamRuby::LessonPaymentCharge' do
|
||||
type 'JamRuby::LessonPaymentCharge'
|
||||
association :user, factory: :user
|
||||
end
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -842,6 +842,45 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe "uncollectables" do
|
||||
let(:user) {FactoryGirl.create(:user)}
|
||||
let(:teacher) {FactoryGirl.create(:teacher_user)}
|
||||
|
||||
|
||||
it "empty" do
|
||||
user.uncollectables.count.should eql 0
|
||||
end
|
||||
|
||||
it "one" do
|
||||
lesson_session = normal_lesson(user, teacher)
|
||||
lesson_session.lesson_payment_charge.user.should eql user
|
||||
lesson_session.lesson_payment_charge.billing_attempts = 1
|
||||
lesson_session.lesson_payment_charge.save!
|
||||
uncollectables = user.uncollectables
|
||||
uncollectables.count.should eql 1
|
||||
uncollectable = uncollectables[0]
|
||||
uncollectable.description.should_not be_nil
|
||||
uncollectable.expected_price_in_cents.should eql 3000
|
||||
uncollectable.is_card_declined?.should be_false
|
||||
end
|
||||
|
||||
it "for monthly" do
|
||||
lesson_session = monthly_lesson(user, teacher)
|
||||
lesson_session.booked_price.should eql 30.00
|
||||
LessonBooking.hourly_check
|
||||
lesson_session.lesson_payment_charge.should be_nil
|
||||
purchases=LessonPackagePurchase.where(user_id: user.id)
|
||||
purchases.count.should eql 1
|
||||
purchases[0].lesson_payment_charge.billing_attempts = 1
|
||||
purchases[0].lesson_payment_charge.save!
|
||||
uncollectables = user.uncollectables
|
||||
uncollectables.count.should eql 1
|
||||
uncollectable = uncollectables[0]
|
||||
uncollectable.description.should_not be_nil
|
||||
uncollectable.expected_price_in_cents.should eql 3000
|
||||
uncollectable.is_card_declined?.should be_false
|
||||
end
|
||||
end
|
||||
=begin
|
||||
describe "update avatar" do
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,41 @@ def normal_lesson(user, teacher, slots = nil)
|
|||
lesson.reload
|
||||
lesson.slot.should eql slots[0]
|
||||
lesson.status.should eql LessonSession::STATUS_APPROVED
|
||||
lesson.music_session.should_not be_nil
|
||||
|
||||
lesson
|
||||
end
|
||||
|
||||
|
||||
def monthly_lesson(user, teacher, slots = nil)
|
||||
|
||||
if slots.nil?
|
||||
slots = []
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
|
||||
end
|
||||
|
||||
if user.stored_credit_card == false
|
||||
user.stored_credit_card = true
|
||||
user.save!
|
||||
end
|
||||
|
||||
booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60)
|
||||
# puts "NORMAL BOOKING #{booking.errors.inspect}"
|
||||
booking.errors.any?.should be_false
|
||||
lesson = booking.lesson_sessions[0]
|
||||
booking.card_presumed_ok.should be_true
|
||||
|
||||
#if user.most_recent_test_drive_purchase.nil?
|
||||
# LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4)
|
||||
#end
|
||||
|
||||
lesson.accept({message: 'Yeah I got this', slot: slots[0]})
|
||||
lesson.errors.any?.should be_false
|
||||
lesson.reload
|
||||
lesson.slot.should eql slots[0]
|
||||
lesson.status.should eql LessonSession::STATUS_APPROVED
|
||||
lesson.music_session.should_not be_nil
|
||||
|
||||
lesson
|
||||
end
|
||||
|
|
@ -2220,6 +2220,16 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getUncollectables(options) {
|
||||
options = options || {}
|
||||
return $.ajax({
|
||||
type: "GET",
|
||||
url: "/api/lesson_sessions/uncollectable",
|
||||
dataType: "json",
|
||||
contentType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getLesson(options) {
|
||||
options = options || {}
|
||||
|
|
@ -2672,6 +2682,7 @@
|
|||
this.counterLessonBooking = counterLessonBooking;
|
||||
this.submitStripe = submitStripe;
|
||||
this.getLessonSessions = getLessonSessions;
|
||||
this.getUncollectables = getUncollectables;
|
||||
this.getLesson = getLesson;
|
||||
this.getLessonAnalysis = getLessonAnalysis;
|
||||
this.updateLessonSessionUnreadMessages = updateLessonSessionUnreadMessages;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ profileUtils = context.JK.ProfileUtils
|
|||
@AccountPaymentHistoryScreen = React.createClass({
|
||||
|
||||
mixins: [
|
||||
#ICheckMixin,
|
||||
ICheckMixin,
|
||||
Reflux.listenTo(AppStore, "onAppInit"),
|
||||
Reflux.listenTo(UserStore, "onUserChanged")
|
||||
]
|
||||
|
|
@ -23,21 +23,62 @@ profileUtils = context.JK.ProfileUtils
|
|||
TILE_PAYMENTS_TO_JAMKAZAM: 'payments to jamkazam'
|
||||
TILE_PAYMENT_METHOD: 'payment method'
|
||||
|
||||
STUDENT_TILES: ['payments to jamkazam', 'payment method']
|
||||
STUDENT_TILES: ['payment method', 'payments to jamkazam']
|
||||
TEACHER_TILES: ['payments to jamkazam', 'payments to you']
|
||||
|
||||
onAppInit: (@app) ->
|
||||
@app.bindScreen('account/paymentHistory', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
|
||||
|
||||
onUserChanged: (userState) ->
|
||||
@setState({user: userState?.user})
|
||||
if !@shouldShowNameSet
|
||||
@shouldShowNameSet = true
|
||||
if userState?.user?
|
||||
username = userState.user.name
|
||||
first_name = userState.user.first_name
|
||||
last_name = userState.user.last_name
|
||||
shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1
|
||||
else
|
||||
shouldShowName = @state.shouldShowName
|
||||
|
||||
@setState({user: userState?.user, shouldShowName: shouldShowName})
|
||||
|
||||
|
||||
componentDidMount: () ->
|
||||
#@checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}]
|
||||
@checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
|
||||
|
||||
@root = $(@getDOMNode())
|
||||
@endOfList = @root.find('.end-of-payments-list')
|
||||
@contentBodyScroller = @root
|
||||
#@iCheckify()
|
||||
@root = $(@getDOMNode())
|
||||
@iCheckify()
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
@iCheckify()
|
||||
|
||||
$expiration = @root.find('input.expiration')
|
||||
if !$expiration.data('payment-applied')
|
||||
$expiration.payment('formatCardExpiry').data('payment-applied', true)
|
||||
$cardNumber = @root.find("input.card-number")
|
||||
if !$cardNumber.data('payment-applied')
|
||||
$cardNumber.payment('formatCardNumber').data('payment-applied', true)
|
||||
$cvv = @root.find("input.cvv")
|
||||
if !$cvv.data('payment-applied')
|
||||
$cvv.payment('formatCardCVC').data('payment-applied', true)
|
||||
|
||||
if @currentNext() == null
|
||||
@contentBodyScroller.off('scroll')
|
||||
if @state[@getCurrentPageName()] == 1 and @getCurrentList().length == 0
|
||||
@endOfList.show()
|
||||
logger.debug("PaymentHistoryScreen: empty search")
|
||||
else if @state[@getCurrentPageName()] > 0
|
||||
logger.debug("end of search")
|
||||
@endOfList.show()
|
||||
else
|
||||
@registerInfiniteScroll(@contentBodyScroller)
|
||||
|
||||
if @activeTile(prevState.selected) != @activeTile() && @getCurrentList().length == 0
|
||||
@refresh()
|
||||
|
||||
|
||||
registerInfiniteScroll:() ->
|
||||
$scroller = @contentBodyScroller
|
||||
|
|
@ -59,24 +100,6 @@ profileUtils = context.JK.ProfileUtils
|
|||
@incrementCurrentPage()
|
||||
@refresh()
|
||||
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
#@iCheckify()
|
||||
|
||||
if @currentNext() == null
|
||||
@contentBodyScroller.off('scroll')
|
||||
if @state[@getCurrentPageName()] == 1 and @getCurrentList().length == 0
|
||||
@endOfList.show()
|
||||
logger.debug("PaymentHistoryScreen: empty search")
|
||||
else if @state[@getCurrentPageName()] > 0
|
||||
logger.debug("end of search")
|
||||
@endOfList.show()
|
||||
else
|
||||
@registerInfiniteScroll(@contentBodyScroller)
|
||||
|
||||
if @activeTile(prevState.selected) != @activeTile() && @getCurrentList().length == 0
|
||||
@refresh()
|
||||
|
||||
checkboxChanged: (e) ->
|
||||
checked = $(e.target).is(':checked')
|
||||
|
||||
|
|
@ -86,6 +109,7 @@ profileUtils = context.JK.ProfileUtils
|
|||
|
||||
beforeHide: (e) ->
|
||||
@screenVisible = false
|
||||
@resetErrors()
|
||||
|
||||
beforeShow: (e) ->
|
||||
|
||||
|
|
@ -93,7 +117,15 @@ profileUtils = context.JK.ProfileUtils
|
|||
@clearResults()
|
||||
@screenVisible = true
|
||||
@refresh()
|
||||
@getUncollectables()
|
||||
|
||||
resetErrors: () ->
|
||||
@setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null})
|
||||
|
||||
checkboxChanged: (e) ->
|
||||
checked = $(e.target).is(':checked')
|
||||
|
||||
@setState({billingInUS: checked})
|
||||
|
||||
refresh: () ->
|
||||
@buildQuery()
|
||||
|
|
@ -116,6 +148,11 @@ profileUtils = context.JK.ProfileUtils
|
|||
.done(@teacherDistributionsDone)
|
||||
.fail(@teacherDistributionsFail)
|
||||
|
||||
getUncollectables: () ->
|
||||
rest.getUncollectables({})
|
||||
.done(@uncollectablesDone)
|
||||
.fail(@uncollectablesFail)
|
||||
|
||||
salesHistoryDone:(response) ->
|
||||
@refreshing = false
|
||||
this.setState({salesNext: response.next, sales: this.state.sales.concat(response.entries)})
|
||||
|
|
@ -132,9 +169,14 @@ profileUtils = context.JK.ProfileUtils
|
|||
@refreshing = false
|
||||
@app.notifyServerError jqXHR, 'Payments to You Unavailable'
|
||||
|
||||
uncollectablesDone: (response) ->
|
||||
this.setState({uncollectables: response})
|
||||
|
||||
uncollectablesFail: (jqXHR) ->
|
||||
@app.notifyServerError jqXHR, 'Unable to fetch uncollectable info'
|
||||
|
||||
clearResults:() ->
|
||||
this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null})
|
||||
this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null, updating: false})
|
||||
|
||||
buildQuery:(page = @getCurrentPage()) ->
|
||||
@currentQuery = this.defaultQuery(page)
|
||||
|
|
@ -182,6 +224,10 @@ profileUtils = context.JK.ProfileUtils
|
|||
else
|
||||
null
|
||||
|
||||
onClick: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
context.location.href = '/client#/account'
|
||||
getInitialState: () ->
|
||||
{
|
||||
user: null,
|
||||
|
|
@ -192,7 +238,11 @@ profileUtils = context.JK.ProfileUtils
|
|||
distributionsNext: null
|
||||
sales: [],
|
||||
distributions: []
|
||||
selected: 'payments to jamkazam'
|
||||
selected: 'payments to jamkazam',
|
||||
updating: false,
|
||||
billingInUS: true,
|
||||
userWantsUpdateCC: false,
|
||||
uncollectables: []
|
||||
}
|
||||
|
||||
onCancel: (e) ->
|
||||
|
|
@ -272,17 +322,147 @@ profileUtils = context.JK.ProfileUtils
|
|||
<a className="btn-next-pager" href="/api/sales?page=1">Next</a>
|
||||
<div className="end-of-payments-list end-of-list">No more payment history</div>
|
||||
<div className="input-aligner">
|
||||
<a className="back button-grey">BACK</a>
|
||||
<a className="back button-grey" onClick={this.onBack}>BACK</a>
|
||||
</div>
|
||||
<br className="clearall" />
|
||||
</div>`
|
||||
|
||||
paymentMethod: () ->
|
||||
disabled = @state.updating || @reuseStoredCard()
|
||||
|
||||
submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating}
|
||||
updateCardClassNames = {'button-grey': true, 'update-btn': true, disabled: disabled && @state.updating}
|
||||
backClassNames = {'button-grey': true, disabled: disabled && @state.updating}
|
||||
|
||||
cardNumberFieldClasses = {field: true, "card-number": true, error: @state.ccError}
|
||||
expirationFieldClasses = {field: true, "expiration": true, error: @state.expiryError}
|
||||
cvvFieldClasses = {field: true, "card-number": true, error: @state.cvvError}
|
||||
inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError}
|
||||
zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError}
|
||||
nameClasses= {field: true, "name": true, error: @state.nameError}
|
||||
formClasses= {stored: @reuseStoredCard()}
|
||||
leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()}
|
||||
rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()}
|
||||
|
||||
if @state.uncollectables.length > 0
|
||||
uncollectable = @state.uncollectables[0]
|
||||
uncollectableMessage = `<div className="uncollectable-msg">A charge for your music lesson with {uncollectable.teacher.name} failed. Please update your credit card information immediately so that we can pay the instructor. If you have called your credit card provider and believe there should be no problem with your card, please email us at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> so that we can figure out what's gone wrong. Thank you!</div>`
|
||||
|
||||
if @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
|
||||
if @state.userWantsUpdateCC
|
||||
header = 'Please update your billing address and payment information below.'
|
||||
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onLockPaymentInfo}>NEVERMIND</a>`
|
||||
actions = `<div className="actions">
|
||||
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
|
||||
{updateCardAction}
|
||||
<a className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
|
||||
</div>`
|
||||
else
|
||||
header = 'You have already entered a credit card in JamKazam.'
|
||||
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
|
||||
actions = `<div className="actions">
|
||||
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
|
||||
{updateCardAction}
|
||||
</div>`
|
||||
else
|
||||
header = 'Please enter your billing address and payment information below.'
|
||||
actions = `<div className="actions">
|
||||
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
|
||||
className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
|
||||
</div>`
|
||||
if @state.shouldShowName && @state.user?.name?
|
||||
username = @state.user?.name
|
||||
nameField =
|
||||
`<div className={classNames(nameClasses)}>
|
||||
<label>Name:</label>
|
||||
<input id="set-user-on-card" disabled={disabled} type="text" name="name" className="name" defaultValue={username}></input>
|
||||
</div>`
|
||||
|
||||
`<div>
|
||||
<div className={classNames(leftColumnClasses)}>
|
||||
{uncollectableMessage}
|
||||
<div className="paymethod-header">{header}</div>
|
||||
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)}>
|
||||
{nameField}
|
||||
<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>
|
||||
{actions}
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>`
|
||||
|
||||
paymentsToJamKazam: () ->
|
||||
rows = []
|
||||
uncollectables = []
|
||||
|
||||
for uncollectable in @state.uncollectables
|
||||
date = context.JK.formatDate(uncollectable.last_billed_at_date, true)
|
||||
paymentMethod = 'Credit Card'
|
||||
amt = uncollectable.expected_price_in_cents
|
||||
displayAmount = ' $' + (amt/100).toFixed(2)
|
||||
|
||||
if uncollectable['is_card_declined?']
|
||||
reason = 'card declined'
|
||||
else if uncollectable['is_card_expired?']
|
||||
reason = 'card expired'
|
||||
else
|
||||
reason = 'charge fail'
|
||||
|
||||
row =
|
||||
`<tr>
|
||||
<td>{date}</td>
|
||||
<td className="capitalize">{paymentMethod}</td>
|
||||
<td>{uncollectable.description}</td>
|
||||
<td className="capitalize">{reason}</td>
|
||||
<td>{displayAmount}</td>
|
||||
</tr>`
|
||||
uncollectables.push(row)
|
||||
|
||||
if uncollectables.length > 0
|
||||
uncollectableTable = `
|
||||
<div className="uncollectables">
|
||||
<div className="uncollectable-msg">You have unpaid lessons, which are listed immediately below. <a onClick={this.selectionMade.bind(this, this.TILE_PAYMENT_METHOD)}>Click here</a> to update your credit card info.</div>
|
||||
<div className="table-header">Unpaid Lessons</div>
|
||||
<table className="payment-table unpaid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CHARGED AT</th>
|
||||
<th>METHOD</th>
|
||||
<th>DESCRIPTION</th>
|
||||
<th>REASON</th>
|
||||
<th>AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{uncollectables}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="table-header second">Payments</div>
|
||||
</div>`
|
||||
for paymentHistory in @getCurrentList()
|
||||
paymentMethod = 'Credit Card'
|
||||
if paymentHistory.sale?
|
||||
|
|
@ -317,6 +497,7 @@ profileUtils = context.JK.ProfileUtils
|
|||
rows.push(row)
|
||||
|
||||
`<div>
|
||||
{uncollectableTable}
|
||||
<table className="payment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -342,13 +523,14 @@ profileUtils = context.JK.ProfileUtils
|
|||
|
||||
selectionMade: (selection, e) ->
|
||||
e.preventDefault()
|
||||
@getUncollectables()
|
||||
@setState({selected: selection})
|
||||
|
||||
activeTile: (selected = this.state.selected) ->
|
||||
if selected?
|
||||
selected
|
||||
else
|
||||
@tiles()[0]
|
||||
@tiles()[-1]
|
||||
|
||||
createTileLink: (i, tile) ->
|
||||
if this.state.selected?
|
||||
|
|
@ -455,4 +637,153 @@ profileUtils = context.JK.ProfileUtils
|
|||
handled = true
|
||||
@setState({updateErrors: errors})
|
||||
|
||||
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 @state.shouldShowName
|
||||
name = @root.find('#set-user-on-card').val()
|
||||
|
||||
if name.indexOf('Anonymous') > -1
|
||||
@setState({nameError: true})
|
||||
error = true
|
||||
|
||||
if !$.payment.validateCardNumber(ccNumber)
|
||||
@setState({ccError: true})
|
||||
error = true
|
||||
|
||||
bits = expiration.split('/')
|
||||
|
||||
if bits.length == 2
|
||||
month = bits[0].trim();
|
||||
year = bits[1].trim()
|
||||
|
||||
month = new Number(month)
|
||||
year = new Number(year)
|
||||
|
||||
if year < 2000
|
||||
year += 2000
|
||||
|
||||
if !$.payment.validateCardExpiry(month, year)
|
||||
@setState({expiryError: true})
|
||||
error = true
|
||||
else
|
||||
@setState({expiryError: true})
|
||||
error = true
|
||||
|
||||
|
||||
cardType = $.payment.cardType(ccNumber)
|
||||
|
||||
if !$.payment.validateCardCVC(cvv, cardType)
|
||||
@setState({cvvError: true})
|
||||
error = true
|
||||
|
||||
if inUS && (!zip? || zip == '')
|
||||
@setState({zipCodeError: true})
|
||||
|
||||
if error
|
||||
return
|
||||
|
||||
data = {
|
||||
number: ccNumber,
|
||||
cvc: cvv,
|
||||
exp_month: month,
|
||||
exp_year: year,
|
||||
}
|
||||
|
||||
@setState({updating: true})
|
||||
|
||||
window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
|
||||
|
||||
stripeResponseHandler: (status, response) ->
|
||||
console.log("stripe response", JSON.stringify(response))
|
||||
|
||||
|
||||
if response.error
|
||||
@setState({updating: false})
|
||||
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})
|
||||
|
||||
#@setState({userWantsUpdateCC: false})
|
||||
#window.UserActions.refresh()
|
||||
@storeCC(response.id)
|
||||
|
||||
storeCC: (token) ->
|
||||
if this.state.billingInUS
|
||||
zip = @root.find('input.zip').val()
|
||||
|
||||
data = {
|
||||
token: token,
|
||||
zip: zip,
|
||||
test_drive: false,
|
||||
normal: false
|
||||
}
|
||||
|
||||
if @state.shouldShowName
|
||||
data.name = @root.find('#set-user-on-card').val()
|
||||
|
||||
@setState({updating: true})
|
||||
rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
|
||||
|
||||
stripeSubmitted: (response) ->
|
||||
@setState({updating: false})
|
||||
|
||||
logger.debug("stripe submitted: " + JSON.stringify(response))
|
||||
|
||||
@setState({userWantsUpdateCC: false})
|
||||
|
||||
#if @state.shouldShowName
|
||||
window.UserActions.refresh()
|
||||
|
||||
if response.uncollectables
|
||||
context.JK.Banner.showAlert('Credit Card Updated', 'Than you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
|
||||
else
|
||||
@app.layout.notify({title: 'Credit Card Updated', text: 'Your credit card info has been updated.'})
|
||||
|
||||
|
||||
stripeSubmitFailure: (jqXHR) ->
|
||||
@setState({updating: false})
|
||||
handled = false
|
||||
if jqXHR.status == 422
|
||||
errors = JSON.parse(jqXHR.responseText)
|
||||
if errors.errors.name?
|
||||
@setState({name: errors.errors.name[0]})
|
||||
handled = true
|
||||
else if errors.errors.user?
|
||||
@app.layout.notify({title: "Can't Update Credit Card", text: "You " + errors.errors.user[0] + '.' })
|
||||
handled = true
|
||||
|
||||
if !handled
|
||||
@app.notifyServerError(jqXHR, 'Credit Card Not Stored')
|
||||
|
||||
onUnlockPaymentInfo: (e) ->
|
||||
e.preventDefault()
|
||||
@setState({userWantsUpdateCC: true})
|
||||
|
||||
onLockPaymentInfo: (e) ->
|
||||
e.preventDefault()
|
||||
@setState({userWantsUpdateCC: false})
|
||||
|
||||
reuseStoredCard: () ->
|
||||
!@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
|
||||
|
||||
})
|
||||
|
|
@ -336,6 +336,7 @@ UserStore = context.UserStore
|
|||
window.location = "/client#/teachers/search"
|
||||
|
||||
stripeSubmitFailure: (jqXHR) ->
|
||||
@setState({updating: false})
|
||||
handled = false
|
||||
if jqXHR.status == 422
|
||||
errors = JSON.parse(jqXHR.responseText)
|
||||
|
|
@ -369,6 +370,7 @@ UserStore = context.UserStore
|
|||
return booked_price.toFixed(2)
|
||||
else
|
||||
return '??'
|
||||
|
||||
render: () ->
|
||||
disabled = @state.updating || @reuseStoredCard()
|
||||
|
||||
|
|
|
|||
|
|
@ -764,6 +764,10 @@
|
|||
|
||||
// returns Fri May 20, 2013
|
||||
context.JK.formatDate = function (dateString, suppressDay) {
|
||||
if (!dateString) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
var date = new Date(dateString);
|
||||
return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@
|
|||
overflow:auto;
|
||||
}
|
||||
|
||||
.payment-table.unpaid {
|
||||
margin-bottom:30px;
|
||||
}
|
||||
.table-header {
|
||||
margin:0 0 10px;
|
||||
color:white;
|
||||
font-weight:bold;
|
||||
&.second {
|
||||
margin:20px 0 10px;
|
||||
}
|
||||
}
|
||||
.content-body {
|
||||
padding-top:29px;
|
||||
height:100%;
|
||||
|
|
@ -115,6 +126,52 @@
|
|||
@include border-box_sizing;
|
||||
width: 100%;
|
||||
}
|
||||
label {
|
||||
display:inline-block;
|
||||
}
|
||||
select {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
form {
|
||||
&.stored {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display:inline-block;
|
||||
width: calc(100% - 150px);
|
||||
@include border_box_sizing;
|
||||
max-width:200px;
|
||||
}
|
||||
.field {
|
||||
position:relative;
|
||||
display:block;
|
||||
margin-top:15px;
|
||||
margin-bottom:25px;
|
||||
|
||||
label {
|
||||
width:150px;
|
||||
}
|
||||
}
|
||||
|
||||
.uncollectable-msg {
|
||||
background-color:black;
|
||||
color:white;
|
||||
padding:20px;
|
||||
margin:20px 0;
|
||||
}
|
||||
|
||||
.paymethod-header {
|
||||
margin:20px 0;
|
||||
}
|
||||
.column-left {
|
||||
margin:20px 0 20px 21px;
|
||||
}
|
||||
.actions {
|
||||
margin-left:-5px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
class ApiLessonSessionsController < ApiController
|
||||
|
||||
before_filter :api_signed_in_user
|
||||
before_filter :lookup_lesson, except: [:index]
|
||||
before_filter :lookup_lesson, except: [:index, :uncollectable]
|
||||
before_filter :is_teacher, only: [:accept]
|
||||
before_filter :is_student, only: []
|
||||
respond_to :json
|
||||
|
|
@ -111,6 +111,9 @@ class ApiLessonSessionsController < ApiController
|
|||
render :json => {}, :status => 200
|
||||
end
|
||||
|
||||
def uncollectable
|
||||
@lesson_payment_charges = current_user.uncollectables
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class ApiStripeController < ApiController
|
|||
@test_drive = data[:test_drive]
|
||||
@normal = data[:normal]
|
||||
@lesson_package_type = data[:lesson_package_type]
|
||||
@uncollectables = data[:uncollectables]
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
object @lesson_payment_charges
|
||||
|
||||
attributes :id, :description, :expected_price_in_cents, :is_card_declined?, :is_card_expired?, :last_billed_at_date
|
||||
|
||||
child(:teacher => :teacher) {
|
||||
attributes :name
|
||||
}
|
||||
|
|
@ -26,4 +26,9 @@ if @lesson_package_type
|
|||
end
|
||||
end
|
||||
|
||||
if @uncollectables
|
||||
node :uncollectables do |lesson|
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -691,6 +691,7 @@ SampleApp::Application.routes.draw do
|
|||
match '/lesson_bookings/unprocessed' => 'api_lesson_bookings#unprocessed', :via => :get
|
||||
match '/lesson_bookings/unprocessed_or_intent' => 'api_lesson_bookings#unprocessed_or_intent', :via => :get
|
||||
|
||||
match '/lesson_sessions/uncollectable' => 'api_lesson_sessions#uncollectable', :via => :get
|
||||
match '/lesson_sessions/:id' => 'api_lesson_sessions#show', :via => :get
|
||||
match '/lesson_sessions/:id/update_unread_messages' => 'api_lesson_sessions#update_unread_messages', :via => :post
|
||||
match '/lesson_sessions/:id/start_time' => 'api_lesson_sessions#start_time', :via => :post
|
||||
|
|
|
|||
|
|
@ -980,8 +980,13 @@ FactoryGirl.define do
|
|||
|
||||
factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do
|
||||
type 'JamRuby::TeacherPaymentCharge'
|
||||
association :user, factory: :user
|
||||
end
|
||||
|
||||
factory :lesson_payment_charge, parent: :charge, class: 'JamRuby::LessonPaymentCharge' do
|
||||
type 'JamRuby::LessonPaymentCharge'
|
||||
association :user, factory: :user
|
||||
end
|
||||
|
||||
factory :teacher_payment, class: 'JamRuby::TeacherPayment' do
|
||||
association :teacher, factory: :teacher_user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe "Account Payment", :js => true, :type => :feature, :capybara_feature => true do
|
||||
|
||||
subject { page }
|
||||
|
||||
let(:user) { FactoryGirl.create(:user, traditional_band: true,paid_sessions: true, paid_sessions_hourly_rate: 1, paid_sessions_daily_rate:1 ) }
|
||||
let(:jam_track) {FactoryGirl.create(:jam_track)}
|
||||
|
||||
|
||||
before(:each) do
|
||||
JamTrackRight.delete_all
|
||||
JamTrack.delete_all
|
||||
AffiliateQuarterlyPayment.delete_all
|
||||
AffiliateMonthlyPayment.delete_all
|
||||
AffiliateTrafficTotal.delete_all
|
||||
UserMailer.deliveries.clear
|
||||
emulate_client
|
||||
sign_in_poltergeist user
|
||||
visit "/client#/account"
|
||||
|
||||
find('div.account-mid.identity')
|
||||
end
|
||||
|
||||
describe "payment history" do
|
||||
it "show 1 sale" do
|
||||
|
||||
sale = Sale.create_jam_track_sale(user)
|
||||
shopping_cart = ShoppingCart.create(user, jam_track)
|
||||
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
|
||||
|
||||
visit "/client#/account"
|
||||
|
||||
find('.account-mid.payments', text: 'You have made 1 purchase.')
|
||||
|
||||
find("#account-payment-history-link").trigger(:click)
|
||||
find('.account-header', text: 'payment history:')
|
||||
find('table tr td', text: '$0.00') # 1st purchase is free
|
||||
|
||||
find('.profile-tile.student a', text: 'payment method').trigger(:click)
|
||||
|
||||
|
||||
fill_in 'card-number', with: '4111111111111111'
|
||||
fill_in 'expiration', with: '11/2016'
|
||||
fill_in 'cvv', with: '111'
|
||||
fill_in 'zip', with: '78759'
|
||||
|
||||
find('.purchase-btn').trigger(:click)
|
||||
|
||||
find('a.update-btn', text: "I'D LIKE TO UPDATE MY PAYMENT INFO").trigger(:click)
|
||||
|
||||
user.reload
|
||||
user.stripe_customer_id.should_not be_nil
|
||||
user.stripe_token.should_not be_nil
|
||||
original_token = user.stripe_token
|
||||
|
||||
fill_in 'card-number', with: '4111111111111111'
|
||||
fill_in 'expiration', with: '11/2016'
|
||||
fill_in 'cvv', with: '111'
|
||||
fill_in 'zip', with: '78759'
|
||||
|
||||
find('.purchase-btn').trigger(:click)
|
||||
|
||||
find('a.update-btn', text: "I'D LIKE TO UPDATE MY PAYMENT INFO").trigger(:click)
|
||||
|
||||
user.reload
|
||||
original_token.should_not eql user.stripe_token
|
||||
end
|
||||
end
|
||||
|
||||
it "handles unpaid lessons" do
|
||||
teacher = FactoryGirl.create(:teacher_user)
|
||||
lesson_session = normal_lesson(user, teacher)
|
||||
lesson_session.lesson_payment_charge.user.should eql user
|
||||
lesson_session.lesson_payment_charge.billing_attempts = 1
|
||||
lesson_session.lesson_payment_charge.save!
|
||||
uncollectables = user.uncollectables
|
||||
uncollectables.count.should eql 1
|
||||
|
||||
visit "/client#/account"
|
||||
|
||||
find('.account-mid.payments', text: 'You have made no purchases.')
|
||||
sleep 2
|
||||
find("#account-payment-history-link").trigger(:click)
|
||||
find('.account-header', text: 'payment history:')
|
||||
|
||||
find('.uncollectable-msg', text: 'You have unpaid lessons')
|
||||
find('.uncollectable-msg a').trigger(:click)
|
||||
|
||||
|
||||
fill_in 'card-number', with: '4111111111111111'
|
||||
fill_in 'expiration', with: '11/2016'
|
||||
fill_in 'cvv', with: '111'
|
||||
fill_in 'zip', with: '78759'
|
||||
|
||||
find('.purchase-btn').trigger(:click)
|
||||
|
||||
find('#banner .dialog-inner', text: 'Your credit card info has been updated')
|
||||
|
||||
# dismiss banner
|
||||
find('a.button-orange', text:'CLOSE').trigger(:click)
|
||||
|
||||
user.reload
|
||||
|
||||
user.stripe_customer_id.should_not be_nil
|
||||
user.stripe_token.should_not be_nil
|
||||
end
|
||||
end
|
||||
|
|
@ -148,27 +148,6 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "payment history" do
|
||||
|
||||
it "show 1 sale" do
|
||||
|
||||
sale = Sale.create_jam_track_sale(user)
|
||||
shopping_cart = ShoppingCart.create(user, jam_track)
|
||||
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
|
||||
|
||||
visit "/client#/account"
|
||||
|
||||
find('.account-mid.payments', text: 'You have made 1 purchase.')
|
||||
|
||||
find("#account-payment-history-link").trigger(:click)
|
||||
find('h2', text: 'payment history:')
|
||||
find('table tr td', text: '$0.00') # 1st purchase is free
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
describe "sessions" do
|
||||
|
||||
before(:each) do
|
||||
|
|
|
|||
|
|
@ -115,3 +115,72 @@ def testdrive_lesson(user, teacher, slots = nil)
|
|||
|
||||
lesson
|
||||
end
|
||||
|
||||
|
||||
|
||||
def normal_lesson(user, teacher, slots = nil)
|
||||
|
||||
if slots.nil?
|
||||
slots = []
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_single)
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_single)
|
||||
end
|
||||
|
||||
if user.stored_credit_card == false
|
||||
user.stored_credit_card = true
|
||||
user.save!
|
||||
end
|
||||
|
||||
booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)
|
||||
# puts "NORMAL BOOKING #{booking.errors.inspect}"
|
||||
booking.errors.any?.should be_false
|
||||
lesson = booking.lesson_sessions[0]
|
||||
booking.card_presumed_ok.should be_true
|
||||
|
||||
#if user.most_recent_test_drive_purchase.nil?
|
||||
# LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4)
|
||||
#end
|
||||
|
||||
lesson.accept({message: 'Yeah I got this', slot: slots[0]})
|
||||
lesson.errors.any?.should be_false
|
||||
lesson.reload
|
||||
lesson.slot.should eql slots[0]
|
||||
lesson.status.should eql LessonSession::STATUS_APPROVED
|
||||
lesson.music_session.should_not be_nil
|
||||
|
||||
lesson
|
||||
end
|
||||
|
||||
|
||||
def monthly_lesson(user, teacher, slots = nil)
|
||||
|
||||
if slots.nil?
|
||||
slots = []
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
|
||||
slots << FactoryGirl.build(:lesson_booking_slot_recurring)
|
||||
end
|
||||
|
||||
if user.stored_credit_card == false
|
||||
user.stored_credit_card = true
|
||||
user.save!
|
||||
end
|
||||
|
||||
booking = LessonBooking.book_normal(user, teacher, slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_MONTHLY, 60)
|
||||
# puts "NORMAL BOOKING #{booking.errors.inspect}"
|
||||
booking.errors.any?.should be_false
|
||||
lesson = booking.lesson_sessions[0]
|
||||
booking.card_presumed_ok.should be_true
|
||||
|
||||
#if user.most_recent_test_drive_purchase.nil?
|
||||
# LessonPackagePurchase.create(user, booking, LessonPackageType.test_drive_4)
|
||||
#end
|
||||
|
||||
lesson.accept({message: 'Yeah I got this', slot: slots[0]})
|
||||
lesson.errors.any?.should be_false
|
||||
lesson.reload
|
||||
lesson.slot.should eql slots[0]
|
||||
lesson.status.should eql LessonSession::STATUS_APPROVED
|
||||
lesson.music_session.should_not be_nil
|
||||
|
||||
lesson
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue