This commit is contained in:
Seth Call 2016-03-25 06:47:35 -05:00
parent cf3d7cddd3
commit 4b4b50cb86
34 changed files with 1256 additions and 426 deletions

View File

@ -29,7 +29,8 @@ CREATE TABLE lesson_bookings (
CREATE TABLE charges (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
amount NUMERIC(8,2) NOT NULL,
amount_in_cents INTEGER NOT NULL,
fee_in_cents INTEGER NOT NULL DEFAULT 0,
type VARCHAR(64) NOT NULL,
sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE,
sent_billing_notices_at TIMESTAMP,
@ -42,6 +43,7 @@ CREATE TABLE charges (
billing_error_detail VARCHAR,
billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE ,
billing_attempts INTEGER NOT NULL DEFAULT 0,
stripe_charge_id VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@ -52,23 +54,16 @@ CREATE TABLE lesson_package_purchases (
user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
teacher_id VARCHAR(64) REFERENCES users(id),
price NUMERIC(8,2),
recurring BOOLEAN NOT NULL DEFAULT FALSE,
year INTEGER,
month INTEGER,
sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE,
sent_billing_notices_at TIMESTAMP,
last_billing_attempt_at TIMESTAMP,
billed BOOLEAN NOT NULL DEFAULT FALSE,
billed_at TIMESTAMP,
billing_error_reason VARCHAR,
billing_error_detail VARCHAR,
billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE ,
billing_attempts INTEGER NOT NULL DEFAULT 0,
charge_id VARCHAR(64) REFERENCES charges(id),
affiliate_partner_id INTEGER REFERENCES affiliate_partners(id),
lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id) NOT NULL,
sent_notices BOOLEAN NOT NULL DEFAULT FALSE,
sent_notices_at TIMESTAMP,
post_processed BOOLEAN NOT NULL DEFAULT FALSE,
post_processed_at TIMESTAMP,
lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@ -94,19 +89,15 @@ CREATE TABLE lesson_sessions (
analysed BOOLEAN NOT NULL DEFAULT FALSE,
analysis JSON,
analysed_at TIMESTAMP,
sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE,
sent_billing_notices_at TIMESTAMP,
last_billing_attempt_at TIMESTAMP,
charge_id VARCHAR(64) REFERENCES charges(id),
success BOOLEAN NOT NULL DEFAULT FALSE,
bill BOOLEAN NOT NULL DEFAULT FALSE,
billed BOOLEAN NOT NULL DEFAULT FALSE,
billed_at TIMESTAMP,
sent_notices BOOLEAN NOT NULL DEFAULT FALSE,
sent_notices_at TIMESTAMP,
post_processed BOOLEAN NOT NULL DEFAULT FALSE,
post_processed_at TIMESTAMP,
billing_error_reason VARCHAR,
billing_error_detail VARCHAR,
billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE ,
billing_attempts INTEGER NOT NULL DEFAULT 0,
affiliate_partner_id INTEGER REFERENCES affiliate_partners(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@ -150,25 +141,31 @@ ALTER TABLE users ADD COLUMN stripe_token VARCHAR(200);
ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(200);
ALTER TABLE users ADD COLUMN stripe_zip_code VARCHAR(200);
ALTER TABLE sales ADD COLUMN stripe_charge_id VARCHAR(200);
ALTER TABLE teachers ADD COLUMN stripe_account_id VARCHAR(200);
ALTER TABLE sale_line_items ADD COLUMN lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id);
-- one is created every time the teacher is paid. N teacher_distributions point to this
CREATE TABLE teacher_payments (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
charge_id VARCHAR(64) REFERENCES charges(id) NOT NULL,
amount NUMERIC(8,2) NOT NULL,
amount_in_cents INTEGER NOT NULL,
fee_in_cents INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- one is created for every bit of money the teacher is due
CREATE TABLE teacher_distributions (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id),
teacher_payment_id VARCHAR(64) REFERENCES teacher_payments(id),
lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id),
lesson_payment_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id),
amount_in_cents INTEGER NOT NULL,
ready BOOLEAN NOT NULL DEFAULT FALSE,
distributed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
);

View File

@ -282,7 +282,11 @@ require "jam_ruby/models/sale_receipt_ios"
require "jam_ruby/models/lesson_session_analyser"
require "jam_ruby/models/lesson_session_monthly_price"
require "jam_ruby/models/teacher_distribution"
require "jam_ruby/models/teacher_payment"
require "jam_ruby/models/charge"
require "jam_ruby/models/teacher_payment_charge"
require "jam_ruby/models/affiliate_payment_charge"
require "jam_ruby/models/lesson_payment_charge"
include Jampb

View File

@ -1226,5 +1226,103 @@
format.html { render :layout => "from_user_mailer" }
end
end
def teacher_distribution_done(teacher_payment)
@teacher_payment = teacher_payment
@teacher = teacher_payment.teacher
email = @teacher.email
@subject = "You have received payment for your participation in JamClass"
unique_args = {:type => "teacher_distribution_done"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@teacher.id])
mail(:to => email, :subject => @subject) do |format|
format.text
format.html
end
end
def teacher_distribution_fail(teacher_payment)
@teacher_payment = teacher_payment
@teacher = teacher_payment.teacher
email = @teacher.email
@card_declined = teacher_payment.is_card_declined?
@card_expired = teacher_payment.is_card_expired?
@bill_date = teacher_payment.last_billed_at_date
@subject = "We were unable to pay you today"
unique_args = {:type => "teacher_distribution_fail"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@teacher.id])
mail(:to => email, :subject => @subject) do |format|
format.text
format.html
end
end
def monthly_recurring_done(lesson_session)
@student = lesson_session.student
@teacher = lesson_session.teacher
@session_name = lesson_session.music_session.name
@session_description = lesson_session.music_session.description
@session_date = lesson_session.slot.pretty_scheduled_start(true)
@session_url = lesson_session.web_url
@lesson_session = lesson_session
email = @student.email
subject = "Your JamClass lesson today with #{@teacher.first_name}"
unique_args = {:type => "student_lesson_normal_no_bill"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@student.id])
mail(:to => email, :subject => subject) do |format|
format.text
format.html { render :layout => "from_user_mailer" }
end
end
def monthly_recurring_no_bill(lesson_session)
@student = lesson_session.student
@teacher = lesson_session.teacher
@session_name = lesson_session.music_session.name
@session_description = lesson_session.music_session.description
@session_date = lesson_session.slot.pretty_scheduled_start(true)
@session_url = lesson_session.web_url
@lesson_session = lesson_session
email = @student.email
subject = "Your lesson with #{@teacher.name} will not be billed"
unique_args = {:type => "student_lesson_normal_done"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
sendgrid_substitute('@USERID', [@student.id])
mail(:to => email, :subject => subject) do |format|
format.text
format.html { render :layout => "from_user_mailer" }
end
end
end
end

View File

@ -0,0 +1,28 @@
<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
<% provide(:photo_url, @teacher.resolved_photo_url) %>
<% content_for :note do %>
<p>
Hello <%= @student.name %>,
</p>
<p>
We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance.
</p>
<p>
<% if !@student.has_rated_teacher(@teacher) %>
If you haven't already done so, please <a href="<%= @teacher.ratings_url %>" style="color:#fc0">rate your teacher</a> now to help other students in the community find the best
instructors.
<% end %>
If you had technical problems during your lesson, or have questions, or would like to make suggestions
on how to improve JamClass, please email us at <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
</p>
<br/>
<p>
Best Regards,<br>Team JamKazam
</p>
<% end %>

View File

@ -0,0 +1,28 @@
<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
<% provide(:photo_url, @teacher.resolved_photo_url) %>
<% content_for :note do %>
<p>
Hello <%= @student.name %>,
</p>
<p>
We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance.
</p>
<p>
<% if !@student.has_rated_teacher(@teacher) %>
If you haven't already done so, please <a href="<%= @teacher.ratings_url %>" style="color:#fc0">rate your teacher</a> now to help other students in the community find the best
instructors.
<% end %>
If you had technical problems during your lesson, or have questions, or would like to make suggestions
on how to improve JamClass, please email us at <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
</p>
<br/>
<p>
Best Regards,<br>Team JamKazam
</p>
<% end %>

View File

@ -0,0 +1,23 @@
<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %>
<% provide(:photo_url, @teacher.resolved_photo_url) %>
<% content_for :note do %>
<p>
Hello <%= @student.name %>,
</p>
<p>You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
<br/>
<br/>
Click the button below to see more information about this session.
</p>
<p>
<a href="<%= @lesson_session.web_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 DETAILS</a>
</p>
<p>
Best Regards,<br>Team JamKazam
</p>
<% end %>

View File

@ -0,0 +1,5 @@
Hello <%= @student.name %>,
You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
To see this lesson, click here: <%= @lesson_session.web_url %>

View File

@ -0,0 +1,41 @@
<% provide(:title, @subject) %>
<p>You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:</p>
<br/>
<% @teacher_payment.teacher_distributions.each do |distribution| %>
<% if distribution.is_test_drive? %>
<h3>You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %></h3>
<p>
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please <a href="<%= distribution.student.student_ratings_url %>" style="color:#fc0">rate your student</a> now to help us monitor for any issues with students who may cause issues for our instructor community.
<% end %>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
</p>
<% elsif distribution.is_normal? %>
<h3>You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %></h3>
<p>
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please <a href="<%= distribution.student.student_ratings_url %>" style="color:#fc0">rate your student</a> now to help us monitor for any issues with students who may cause issues for our instructor community.
<% end %>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
</p>
<% elsif distribution.is_monthly? %>
<h3>You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %></h3>
<p>
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please <a href="<%= distribution.student.student_ratings_url %>" style="color:#fc0">rate your student</a> now to help us monitor for any issues with students who may cause issues for our instructor community.
<% end %>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at <a href="mailto:support@jamkazam.com" style="color:#fc0">support@jamkazam.com</a>.
</p>
<% else %>
Unknown payment type.
<% end %>
<br/>
<br/>
<% end %>
Best Regards,<br/>
JamKazam

View File

@ -0,0 +1,31 @@
<% provide(:title, @subject) %>
You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:
<% @teacher_payment.teacher_distributions.each do |distribution| %>
<% if distribution.is_test_drive? %>
You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>.
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
<% end%>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
<% elsif distribution.is_normal? %>
You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>.
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
<% end%>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
<% elsif distribution.is_monthly? %>
You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>.
<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
<% end%>
If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
<% else %>
Unknown payment type.
<% end %>
<% end %>
Best Regards,
JamKazam

View File

@ -0,0 +1,15 @@
<% provide(:title, @subject) %>
<p>
<% if @card_declined %>
When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
<% elsif @card_expired %>
When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
<% else %>
For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
<% end %>
</p>
<br/>
Best Regards,<br/>
JamKazam

View File

@ -0,0 +1,12 @@
<% provide(:title, @subject) %>
<% if @card_declined %>
When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
<% elsif @card_expired %>
When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
<% else %>
For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
<% end %>
Best Regards,
JamKazam

View File

@ -128,7 +128,17 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
else
false
end
end
def should_attribute_payment?(teacher_payment)
if created_within_affiliate_window(teacher_payment.teacher, teacher_payment.created_at)
product_info = shopping_cart.product_info
# subtract the total quantity from the freebie quantity, to see how much we should attribute to them
real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i
{fee_in_cents: (product_info[:price] * 100 * real_quantity * rate.to_f).round}
else
false
end
end
def cumulative_earnings_in_dollars

View File

@ -0,0 +1,51 @@
module JamRuby
class AffiliatePaymentCharge < Charge
has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id
def distribution
@distribution ||= teacher_payment.teacher_distribution
end
def max_retries
9999999
end
def teacher
@teacher ||= teacher_payment.teacher
end
def charged_user
teacher
end
def do_charge
# source will let you supply a token. But... how to get a token in this case?
stripe_charge = Stripe::Charge.create(
:amount => amount_in_cents,
:currency => "usd",
:customer => APP_CONFIG.stripe[:source_customer],
:description => construct_description,
:destination => teacher.teacher.stripe_account_id,
:application_fee => fee_in_cents,
)
stripe_charge
end
def do_send_notices
UserMailer.teacher_distribution_done(teacher_payment)
end
def do_send_unable_charge
UserMailer.teacher_distribution_fail(teacher_payment)
end
def construct_description
teacher_payment.teacher_distribution.description
end
end
end

View File

@ -1,25 +1,40 @@
module JamRuby
class Charge
validates :sent_notices, inclusion: {in: [true, false]}
class Charge < ActiveRecord::Base
validates :sent_billing_notices, inclusion: {in: [true, false]}
def max_retries
raise "not implemented"
end
def do_charge
def do_charge(force)
raise "not implemented"
end
def do_send_notices
raise "not implemented"
end
def do_send_unable_charge
raise "not implemented"
end
def charge_retry_hours
24
end
def charged_user
raise "not implemented"
end
def bill_lesson
def charge(force = false)
stripe_charge = nil
if !self.billed
# check if we can bill at the moment
if last_billing_attempt_at && (24.hours.ago < last_billing_attempt_at)
return
if !force && last_billing_attempt_at && (charge_retry_hours.hours.ago < last_billing_attempt_at)
return false
end
if !billing_should_retry
return
if !force && !billing_should_retry
return false
end
@ -31,34 +46,24 @@ module JamRuby
begin
do_charge
if sale.errors.any?
self.billing_error_reason = 'sale_error'
self.billing_error_detail = sale.errors.inspect
line_item = sale.sale_line_items[0]
if line_item && line_item.errors.any?
self.billing_error_detail = "#{self.billing_error_detail}\n\n#{line_item.errors.inspect}"
end
self.save(validate: false)
return false
else
self.billed = true
self.billed_at = Time.now
self.save(validate: false)
end
stripe_charge = do_charge(force)
self.stripe_charge_id = stripe_charge.id
self.billed = true
self.billed_at = Time.now
self.save(validate: false)
rescue Stripe::StripeError => e
stripe_handler(e)
subject = "Unable to charge user #{student.email} for lesson #{self.id} (stripe)"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (stripe)"
body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
UserMailer.student_unable_charge(self)
do_send_unable_charge
return false
rescue Exception => e
subject = "Unable to charge user #{student.email} for lesson #{self.id} (unhandled)"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
subject = "Unable to charge user #{charged_user.email} for lesson #{self.id} (unhandled)"
body = "user=#{charged_user.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
unhandled_handler(e)
return false
@ -70,8 +75,7 @@ module JamRuby
# If the charge is successful, then we post the charge to the students payment history,
# and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email
UserMailer.student_lesson_normal_done(self).deliver
UserMailer.teacher_lesson_normal_done(self).deliver
do_send_notices
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
@ -80,8 +84,7 @@ module JamRuby
self.save(validate: false)
end
return true
return stripe_charge
end
def unhandled_handler(e, reason = 'unhandled_exception')

View File

@ -4,15 +4,14 @@ module JamRuby
@@log = Logging.logger[LessonPackagePurchase]
def name
lesson_package_type.sale_display
end
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge
# who purchased the lesson package?
belongs_to :user, class_name: "JamRuby::User", :foreign_key => "user_id", inverse_of: :lesson_purchases
belongs_to :lesson_package_type, class_name: "JamRuby::LessonPackageType"
belongs_to :teacher, class_name: "JamRuby::User"
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id
has_one :sale_line_item, class_name: "JamRuby::SaleLineItem"
@ -20,10 +19,18 @@ module JamRuby
validates :lesson_package_type, presence: true
validates :price, presence: true
after_save :after_save
after_create :add_test_drives
after_create :create_charge
def after_save
def create_charge
self.lesson_payment_charge = LessonPaymentCharge.new
lesson_payment_charge.amount_in_cents = 0
lesson_payment_charge.fee_in_cents = 0
lesson_payment_charge.lesson_package_purchase = self
lesson_payment_charge.save!
end
def add_test_drives
if self.lesson_package_type.is_test_drive?
new_test_drives = user.remaining_test_drives + 4
User.where(id: user.id).update_all(remaining_test_drives: new_test_drives)
@ -32,8 +39,13 @@ module JamRuby
end
def name
lesson_package_type.sale_display
end
def amount_charged
sale_line_item.sale.recurly_total_in_cents / 100.0
lesson_payment_charge.amount_in_cents / 100.0
end
def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil)
@ -69,144 +81,53 @@ module JamRuby
def description(lesson_booking)
lesson_package_type.description(lesson_booking)
end
end
def month_name
if recurring
Date.new(year, month, 1).strftime('%B')
else
'non-monthly paid lesson'
def stripe_description(lesson_booking)
description(lesson_booking)
end
end
def student
user
end
def bill_monthly(force = false)
# let's attempt to bill for the month
if !self.billed
# check if we can bill at the moment
if !force && last_billing_attempt_at && (24.hours.ago < last_billing_attempt_at)
return
def month_name
if recurring
Date.new(year, month, 1).strftime('%B')
else
'non-monthly paid lesson'
end
end
if !force && !billing_should_retry
return
def student
user
end
def bill_monthly(force = false)
lesson_payment_charge.charge(force)
if lesson_payment_charge.billed
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
# bill the user right now. if it fails, move on; will be tried again
self.billing_attempts = self.billing_attempts + 1
self.billing_should_retry = self.billing_attempts < 5
self.last_billing_attempt_at = Time.now
self.save(validate: false)
begin
sale = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, nil, self)
if sale.errors.any?
self.billing_error_reason = 'sale_error'
self.billing_error_detail = sale.errors.inspect
line_item = sale.sale_line_items[0]
if line_item && line_item.errors.any?
self.billing_error_detail = "#{self.billing_error_detail}\n\n#{line_item.errors.inspect}"
end
self.save(validate: false)
return false
else
self.billed = true
self.billed_at = Time.now
self.save(validate: false)
lesson_booking.unsuspend! if lesson_booking.is_suspended?
end
rescue Stripe::StripeError => e
stripe_handler(e)
if !billing_should_retry
lesson_booking.suspend!
end
subject = "Unable to charge user #{student.email} for lesson #{self.id} (stripe=#{billing_error_reason})"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
UserMailer.student_unable_charge_monthly(self)
if lesson_booking.is_suspended?
UserMailer.teacher_unable_charge_monthly(self)
end
return false
rescue Exception => e
subject = "Unable to charge user #{student.email} for lesson #{self.id} (unhandled)"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
unhandled_handler(e)
return false
end
end
if !self.sent_billing_notices
# If the charge is successful, then we post the charge to the students payment history,
# and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email
def is_card_declined?
billed == false && billing_error_reason == 'card_declined'
end
UserMailer.student_lesson_monthly_charged(self).deliver
UserMailer.teacher_lesson_monthly_charged(self).deliver
def is_card_expired?
billed == false && billing_error_reason == 'card_expired'
end
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(validate: false)
def last_billed_at_date
last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at
end
return true
end
def stripe_handler(e)
msg = e.to_s
if msg.include?('declined')
self.billing_error_reason = 'card_declined'
self.billing_error_detail = msg
elsif msg.include?('expired')
self.billing_error_reason = 'card_expired'
self.billing_error_detail = msg
elsif msg.include?('processing')
self.billing_error_reason = 'processing_error'
self.billing_error_detail = msg
else
self.billing_error_reason = 'stripe'
self.billing_error_detail = msg
def update_payment_url
APP_CONFIG.external_root_url + "/client#/jamclass/update-payment"
end
self.save(validate: false)
end
def is_card_declined?
billed == false && billing_error_reason == 'card_declined'
end
def is_card_expired?
billed == false && billing_error_reason == 'card_expired'
end
def last_billed_at_date
last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at
end
def update_payment_url
APP_CONFIG.external_root_url + "/client#/jamclass/update-payment"
end
end

View File

@ -62,6 +62,10 @@ module JamRuby
end
end
def stripe_description(lesson_booking)
description(lesson_booking)
end
def is_single_free?
id == SINGLE_FREE
end

View File

@ -0,0 +1,80 @@
module JamRuby
class LessonPaymentCharge < Charge
has_one :lesson_session, class_name: "JamRuby::LessonSession", foreign_key: :charge_id
has_one :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase", foreign_key: :charge_id
def max_retries
5
end
def charged_user
@charged_user ||= target.student
end
def resolve_target
if is_lesson?
lesson_session
else
lesson_package_purchase
end
end
def target
@target ||= resolve_target
end
def lesson_booking
@lesson_booking ||= target.lesson_booking
end
def student
charged_user
end
def is_lesson?
!lesson_session.nil?
end
def do_charge(force)
if is_lesson?
result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, lesson_session)
else
result = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, nil, lesson_package_purchase, force)
lesson_booking.unsuspend! if lesson_booking.is_suspended?
end
stripe_charge = result[:stripe_charge]
self.amount_in_cents = stripe_charge.amount
self.save(validate: false)
stripe_charge
end
def do_send_notices
if is_lesson?
UserMailer.student_lesson_normal_done(lesson_session).deliver
UserMailer.teacher_lesson_normal_done(lesson_session).deliver
else
UserMailer.student_lesson_monthly_charged(lesson_package_purchase).deliver
UserMailer.teacher_lesson_monthly_charged(lesson_package_purchase).deliver
end
end
def do_send_unable_charge
if is_lesson?
UserMailer.student_unable_charge(lesson_session)
else
if !billing_should_retry
lesson_booking.suspend!
end
UserMailer.student_unable_charge_monthly(self)
if lesson_booking.is_suspended?
# let the teacher know that we are having problems collecting from the student
UserMailer.teacher_unable_charge_monthly(self)
end
end
end
end
end

View File

@ -5,8 +5,13 @@ module JamRuby
attr_accessor :accepting, :creating, :countering, :countered_slot, :countered_lesson
@@log = Logging.logger[LessonSession]
delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge
delegate :is_test_drive?, :is_single_free?, :is_normal?, to: :lesson_booking
STATUS_REQUESTED = 'requested'
STATUS_CANCELED = 'canceled'
STATUS_MISSED = 'missed'
@ -26,6 +31,7 @@ module JamRuby
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id
belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot"
validates :duration, presence: true, numericality: {only_integer: true}
@ -38,14 +44,22 @@ module JamRuby
validates :teacher_canceled, inclusion: {in: [true, false]}
validates :student_canceled, inclusion: {in: [true, false]}
validates :success, inclusion: {in: [true, false]}
validates :bill, inclusion: {in: [true, false]}
validates :billed, inclusion: {in: [true, false]}
validates :sent_notices, inclusion: {in: [true, false]}
validates :post_processed, inclusion: {in: [true, false]}
validate :validate_creating, :if => :creating
validate :validate_accepted, :if => :accepting
after_save :after_counter, :if => :countering
after_save :manage_slot_changes
after_create :create_charge
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_session = self
lesson_payment_charge.save!
end
def manage_slot_changes
# if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted .
@ -76,7 +90,7 @@ module JamRuby
end
def self.complete_sessions
MusicSession.joins(lesson_session: :lesson_booking).where('session_removed_at IS NOT NULL').where('analysed = true').where('post_processed = false').where('billing_should_retry = true').each do |music_session|
MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('session_removed_at IS NOT NULL').where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session|
lession_session = music_session.lesson_session
lession_session.session_completed
end
@ -94,9 +108,9 @@ module JamRuby
self.analysed_at = Time.now
self.analysed = true
if lesson_booking.requires_per_session_billing?
self.bill = true
end
# if lesson_booking.requires_per_session_billing?
# self.bill = true
# end
if self.save
# send out emails appropriate for this type of session
@ -106,11 +120,7 @@ module JamRuby
def amount_charged
if lesson_package_purchase
lesson_package_purchase.sale_line_item.sale.recurly_total_in_cents / 100.0
else
nil
end
lesson_payment_charge.amount_in_cents / 100.0
end
def analysis_to_json(analysis)
@ -143,153 +153,43 @@ module JamRuby
return
end
begin
if lesson_booking.is_test_drive?
test_drive_completed
elsif lesson_booking.is_normal?
if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment?
recurring_completed
else
normal_lesson_completed
end
if lesson_booking.is_test_drive?
test_drive_completed
elsif lesson_booking.is_normal?
if lesson_booking.is_weekly_payment? || lesson_booking.is_monthly_payment?
recurring_completed
else
normal_lesson_completed
end
rescue Exception => e
self.unhandled_handler(e)
end
end
end
def bill_lesson
if !self.billed
lesson_payment_charge.charge
# check if we can bill at the moment
if last_billing_attempt_at && (24.hours.ago < last_billing_attempt_at)
return
end
if !billing_should_retry
return
end
# bill the user right now. if it fails, move on; will be tried again
self.billing_attempts = self.billing_attempts + 1
self.billing_should_retry = self.billing_attempts < 5
self.last_billing_attempt_at = Time.now
self.save(validate: false)
begin
sale = Sale.purchase_lesson(student, lesson_booking, lesson_booking.lesson_package_type, self)
if sale.errors.any?
self.billing_error_reason = 'sale_error'
self.billing_error_detail = sale.errors.inspect
line_item = sale.sale_line_items[0]
if line_item && line_item.errors.any?
self.billing_error_detail = "#{self.billing_error_detail}\n\n#{line_item.errors.inspect}"
end
self.save(validate: false)
return false
else
self.billed = true
self.billed_at = Time.now
self.save(validate: false)
end
rescue Stripe::StripeError => e
stripe_handler(e)
subject = "Unable to charge user #{student.email} for lesson #{self.id} (stripe)"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
UserMailer.student_unable_charge(self)
return false
rescue Exception => e
subject = "Unable to charge user #{student.email} for lesson #{self.id} (unhandled)"
body = "teacher=#{teacher.email}\n\nbilling_error_reason=#{billing_error_reason}\n\nbilling_error_detail = #{billing_error_detail}"
AdminMailer.alerts({subject: subject, body: body})
unhandled_handler(e)
return false
end
end
if !self.sent_billing_notices
# If the charge is successful, then we post the charge to the students payment history,
# and associate the charge with the lesson, so that everyone knows the student has paid, and we send an email
UserMailer.student_lesson_normal_done(self).deliver
UserMailer.teacher_lesson_normal_done(self).deliver
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
if lesson_payment_charge.billed
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(validate: false)
self.save(:validate => false)
end
return true
end
def unhandled_handler(e, reason = 'unhandled_exception')
self.billing_error_reason = reason
if e.cause
self.billing_error_detail = e.cause.to_s + "\n" + e.cause.backtrace.join("\n\t") if e.cause.backtrace
self.billing_error_detail << "\n\n"
self.billing_error_detail << e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace
else
self.billing_error_detail = e.to_s + "\n" + e.backtrace.join("\n\t") if e.backtrace
end
self.save(validate: :false)
end
def is_card_declined?
billed == false && billing_error_reason == 'card_declined'
end
def is_card_expired?
billed == false && billing_error_reason == 'card_expired'
end
def last_billed_at_date
last_billing_attempt_at.strftime("%B %d, %Y") if last_billing_attempt_at
end
def stripe_handler(e)
msg = e.to_s
if msg.include?('declined')
self.billing_error_reason = 'card_declined'
self.billing_error_detail = msg
elsif msg.include?('expired')
self.billing_error_reason = 'card_expired'
self.billing_error_detail = msg
elsif msg.include?('processing')
self.billing_error_reason = 'processing_error'
self.billing_error_detail = msg
else
self.billing_error_reason = 'stripe'
self.billing_error_detail = msg
end
self.save(validate: false)
end
def test_drive_completed
if !sent_billing_notices
if !sent_notices
if success
student.test_drive_succeeded(self)
else
student.test_drive_failed(self)
end
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
@ -301,12 +201,12 @@ module JamRuby
if lesson_booking.is_monthly_payment?
# monthly payments are handled at beginning of month; just poke with email, and move on
if !sent_billing_notices
if !sent_notices
# not in spec; just poke user and tell them we saw it was successfully completed
UserMailer.monthly_recurring_done(user, lesson_session).deliver
UserMailer.monthly_recurring_done(self).deliver
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
@ -317,21 +217,21 @@ module JamRuby
else
if lesson_booking.is_monthly_payment?
# bad session; just poke user
if !sent_billing_notices
UserMailer.monthly_recurring_no_bill(user, self).deliver
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
if !sent_notices
UserMailer.monthly_recurring_no_bill(self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
end
else
if !sent_billing_notices
if !sent_notices
# bad session; just poke user
UserMailer.student_weekly_recurring_no_bill(user, self).deliver
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
UserMailer.student_weekly_recurring_no_bill(student, self).deliver
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
@ -345,11 +245,11 @@ module JamRuby
if success
bill_lesson
else
if !sent_billing_notices
if !sent_notices
UserMailer.student_lesson_normal_no_bill(self).deliver
UserMailer.teacher_lesson_no_bill(self).deliver
self.sent_billing_notices = true
self.sent_billing_notices_at = Time.now
self.sent_notices = true
self.sent_notices_at = Time.now
self.post_processed = true
self.post_processed_at = Time.now
self.save(:validate => false)
@ -587,6 +487,14 @@ module JamRuby
Notification.send_lesson_message('counter', self, slot.is_teacher_created?)
end
def description(lesson_booking)
lesson_booking.lesson_package_type.description(lesson_booking)
end
def stripe_description(lesson_booking)
description(lesson_booking)
end
def home_url
APP_CONFIG.external_root_url + "/client#/jamclass"
end

View File

@ -217,7 +217,8 @@ module JamRuby
end
# this is easy to make generic, but right now, it just purchases lessons
def self.purchase_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil)
def self.purchase_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false)
stripe_charge = nil
sale = nil
# everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
Sale.transaction(:requires_new => true) do
@ -226,15 +227,13 @@ module JamRuby
if sale.valid?
price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, lesson_session, lesson_package_purchase)
sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type)
price_info = charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force)
if !sale_line_item.valid?
raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}"
end
sale_line_item.lesson_package_purchase = price_info[:purchase]
sale_line_item.save
# sale.source = 'stripe'
sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
sale.recurly_tax_in_cents = price_info[:tax_in_cents]
@ -242,17 +241,25 @@ module JamRuby
sale.recurly_currency = price_info[:currency]
sale.stripe_charge_id = price_info[:charge_id]
sale.save
stripe_charge = price_info[:charge]
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
raise "invalid sale object"
end
end
sale
{sale: sale, stripe_charge: stripe_charge}
end
def self.charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session = nil, lesson_package_purchase = nil, force = false)
if lesson_package_purchase
target = lesson_package_purchase
elsif lesson_session
target = lesson_session
else
target = lesson_package_type
end
def self.charge_stripe_for_lesson(current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil)
current_user.sync_stripe_customer
purchase = lesson_package_purchase
@ -276,20 +283,23 @@ module JamRuby
tax_in_cents = (subtotal_in_cents * tax_percent).round
total_in_cents = subtotal_in_cents + tax_in_cents
charge = Stripe::Charge.create(
stripe_charge = Stripe::Charge.create(
:amount => total_in_cents,
:currency => "usd",
:customer => current_user.stripe_customer_id,
:description => purchase.description(lesson_booking)
:description => target.stripe_description(lesson_booking)
)
sale_line_item.lesson_package_purchase = purchase
sale_line_item.save
price_info = {}
price_info[:subtotal_in_cents] = subtotal_in_cents
price_info[:tax_in_cents] = tax_in_cents
price_info[:total_in_cents] = total_in_cents
price_info[:currency] = 'USD'
price_info[:charge_id] = charge.id
price_info[:change] = charge
price_info[:charge_id] = stripe_charge.id
price_info[:charge] = stripe_charge
price_info[:purchase] = purchase
price_info
end

View File

@ -16,7 +16,7 @@ module JamRuby
has_many :lesson_sessions, :class_name => "JamRuby::LessonSession"
has_many :lesson_package_purchases, :class_name => "JamRuby::LessonPackagePurchase"
has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target
has_one :user, :class_name => 'JamRuby::User'
has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id
validates :user, :presence => true
validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction

View File

@ -1,14 +1,56 @@
module JamRuby
class TeacherPayment
class TeacherDistribution < ActiveRecord::Base
belongs_to :teacher, class: "JamRuby::User"
belongs_to :charge, class: "JamRuby::Charge"
belongs_to :teacher, class_name: "JamRuby::User", foreign_key: "teacher_id"
belongs_to :teacher_payment, class_name: "JamRuby::TeacherPayment"
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
validates :teacher, presence: true
validates :charge, presence: true
validates :amount, presence: true
validates :amount_in_cents, presence: true
def amount
amount_in_cents / 100.0
end
def student
if lesson_session
lesson_session.student
else
lesson_package_purchase.student
end
end
def month_name
lesson_package_purchase.month_name
end
def is_test_drive?
lesson_session && lesson_session.is_test_drive?
end
def is_normal?
lesson_session && !lesson_session.is_test_drive?
end
def is_monthly?
!lesson_package_purchase.nil?
end
def description
if lesson_session
if lesson_session.lesson_booking.is_test_drive?
"Test Drive session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}"
elsif lesson_session.lesson_booking.is_normal?
if lesson_session.lesson_booking.is_weekly_payment? || lesson_session.lesson_booking.is_monthly_payment?
raise "Should not be here"
else
"A session with #{lesson_session.lesson_booking.student.name} on #{lesson_session.scheduled_start.to_date}"
end
end
else
"Monthly session for the month of #{lesson_package_purchase.month_name} with #{lesson_package_purchase.lesson_booking.student.name}"
end
end
end
end

View File

@ -0,0 +1,113 @@
module JamRuby
class TeacherPayment < ActiveRecord::Base
belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id
belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id
has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution"
def self.daily_check
teacher_payments
end
def teacher_distributions
[teacher_distribution]
end
def self.pending_teacher_payments
User.select(['users.id']).joins(:teacher).joins(:teacher_distributions).where('teachers.stripe_account_id IS NOT NULL').where('teacher_distributions.distributed = false').where('teacher_distributions.ready = true').uniq
end
def self.teacher_payments
pending_teacher_payments.each do |row|
teacher = User.find(row['id'])
TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).each do |distribution|
payment = TeacherPayment.charge(teacher)
if payment.nil? || !payment.teacher_payment_charge.billed
puts "NOT BILLED"
break
end
end
end
end
def amount
amount_in_cents / 100.0
end
def is_card_declined?
teacher_payment_charge.is_card_declined?
end
def is_card_expired?
teacher_payment_charge.is_card_expired?
end
def last_billed_at_date
teacher_payment_charge.last_billed_at_date
end
def charge_retry_hours
22 # thi is only run once a day, so we make sure that slightly differences in trigger time don't cause a skip for a day
end
def calculate_teacher_fee
if teacher_distribution.is_test_drive?
0
else
(amount_in_cents * 0.28).round
end
end
# will find, for a given teacher, an outstading unsuccessful payment or make a new one.
# it will then associate a charge with it, and then execute the charge.
def self.charge(teacher)
payment = TeacherPayment.joins(:teacher_payment_charge).where('teacher_payments.teacher_id = ?', teacher.id).where('charges.billed = false').order(:created_at).first
if payment.nil?
payment = TeacherPayment.new
payment.teacher = teacher
else
payment = TeacherPayment.find(payment.id)
end
if payment.teacher_distribution.nil?
teacher_distribution = TeacherDistribution.where(teacher_id: teacher.id).where(ready:true).where(distributed: false).order(:created_at).first
if teacher_distribution.nil?
return
end
payment.teacher_distribution = teacher_distribution
end
payment.amount_in_cents = payment.teacher_distribution.amount_in_cents
payment.fee_in_cents = payment.calculate_teacher_fee
if payment.teacher_payment_charge.nil?
charge = TeacherPaymentCharge.new
charge.amount_in_cents = payment.amount_in_cents
charge.fee_in_cents = payment.fee_in_cents
charge.teacher_payment = payment
payment.teacher_payment_charge = charge
# charge.save!
else
charge = payment.teacher_payment_charge
charge.amount_in_cents = payment.amount_in_cents
charge.fee_in_cents = payment.fee_in_cents
charge.save!
end
payment.save!
payment.teacher_payment_charge.charge
if payment.teacher_payment_charge.billed
payment.teacher_distribution.distributed = true
payment.teacher_distribution.save!
end
payment
end
end
end

View File

@ -1,47 +1,52 @@
module JamRuby
class TeacherPaymentCharge < Charge
has_one :teacher_payment, class: "JamRuby::TeacherDistribution"
has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :charge_id
def distribution
@distribution ||= teacher_payment.teacher_distribution
end
def max_retries
1
9999999
end
def teacher
@teacher ||= teacher_distribution.teacher
@teacher ||= teacher_payment.teacher
end
def amount_in_cents
amount * 100
def charged_user
teacher
end
def uncollected_charges
T.where(teacher_id: teacher.id).where(distributed: false).where(billed:true).where(type: 'JamRuby::StudentLessonCharge')
end
def do_charge
@uncollected_charges = uncollected_charges
charge = Stripe::Charge.create(
def do_charge(force)
# source will let you supply a token. But... how to get a token in this case?
stripe_charge = Stripe::Charge.create(
:amount => amount_in_cents,
:currency => "usd",
:source => APP_CONFIG.stripe_charge_token,
:customer => APP_CONFIG.stripe[:source_customer],
:description => construct_description,
:destination => teacher.stripe_account_id
:destination => teacher.teacher.stripe_account_id,
:application_fee => fee_in_cents,
)
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[:change] = charge
price_info[:purchase] = purchase
price_info
stripe_charge
end
def do_send_notices
UserMailer.teacher_distribution_done(teacher_payment)
end
def do_send_unable_charge
UserMailer.teacher_distribution_fail(teacher_payment)
end
def construct_description
teacher_distribution.
teacher_payment.teacher_distribution.description
end
end

View File

@ -174,6 +174,8 @@ module JamRuby
has_many :lesson_purchases, :class_name => "JamRuby::LessonPackagePurchase", :foreign_key => "user_id", inverse_of: :user
has_many :student_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "user_id", inverse_of: :user
has_many :teacher_lesson_bookings, :class_name => "JamRuby::LessonBooking", :foreign_key => "teacher_id", inverse_of: :teacher
has_many :teacher_distributions, :class_name => "JamRuby::TeacherDistribution", :foreign_key => "teacher_id", inverse_of: :teacher
has_many :teacher_payments, :class_name => "JamRuby::TeacherPayment", :foreign_key => "teacher_id", inverse_of: :teacher
# Shopping carts
has_many :shopping_carts, :class_name => "JamRuby::ShoppingCart"
@ -192,7 +194,7 @@ module JamRuby
has_one :musician_search, :class_name => 'JamRuby::MusicianSearch'
has_one :band_search, :class_name => 'JamRuby::BandSearch'
belongs_to :teacher, :class_name => 'JamRuby::Teacher'
belongs_to :teacher, :class_name => 'JamRuby::Teacher', foreign_key: :teacher_id
has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession"
@ -1820,6 +1822,14 @@ module JamRuby
end
end
def should_attribute_payment?(teacher_payment)
if affiliate_referral
referral_info = affiliate_referral.should_attribute_payment?(teacher_payment)
else
false
end
end
def redeem_free_credit
using_free_credit = false
if self.has_redeemable_jamtrack
@ -1908,7 +1918,8 @@ module JamRuby
booking = card_approved(params[:token], params[:zip])
if params[:test_drive]
self.reload
test_drive = Sale.purchase_test_drive(self, booking)
result = Sale.purchase_test_drive(self, booking)
test_drive = result[:sale]
elsif params[:normal]
self.reload
end
@ -1953,16 +1964,28 @@ module JamRuby
end
def has_rated_teacher(teacher)
if teacher.is_a?(JamRuby::User)
teacher = teacher.teacher
end
Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s).count > 0
end
def has_rated_student(student)
Review.where(target_id: student.id).where(target_type: "JamRuby::User").count > 0
end
def teacher_profile_url
"#{APP_CONFIG.external_root_url}/client#/profile/teacher/#{id}"
end
def ratings_url
"#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/teacher/#{id}"
end
def student_ratings_url
"#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/#{id}"
end
def self.search_url
"#{APP_CONFIG.external_root_url}/client#/jamclass/searchOptions"
end

View File

@ -8,6 +8,8 @@ module JamRuby
def self.perform
@@log.debug("waking up")
TeacherPayment.daily_check
bounced_emails
calendar_manager = CalendarManager.new

View File

@ -104,6 +104,8 @@ FactoryGirl.define do
factory :teacher, :class => JamRuby::Teacher do
association :user, factory: :user
price_per_lesson_60_cents 3000
price_per_month_60_cents 3000
end
factory :musician_instrument, :class => JamRuby::MusicianInstrument do
@ -963,7 +965,6 @@ FactoryGirl.define do
end
end
factory :lesson_session, class: 'JamRuby::LessonSession' do
ignore do
@ -980,6 +981,31 @@ FactoryGirl.define do
#teacher_complete true
#student_complete true
end
factory :charge, class: 'JamRuby::Charge' do
type 'JamRuby::Charge'
amount_in_cents 1000
end
factory :teacher_payment_charge, parent: :charge, class: 'JamRuby::TeacherPaymentCharge' do
type 'JamRuby::TeacherPaymentCharge'
end
factory :teacher_payment, class: 'JamRuby::TeacherPayment' do
association :teacher, factory: :teacher_user
association :teacher_payment_charge, factory: :teacher_payment_charge
amount_in_cents 1000
end
# you gotta pass either lesson_session or lesson_package_purchase for this to make sense
factory :teacher_distribution, class: 'JamRuby::TeacherDistribution' do
association :teacher, factory: :teacher_user
association :teacher_payment, factory: :teacher_payment
ready false
amount_in_cents 1000
end
factory :ip_blacklist, class: "JamRuby::IpBlacklist" do
remote_ip '1.1.1.1'

View File

@ -35,7 +35,7 @@ describe "Monthly Recurring Lesson Flow" do
booking.card_presumed_ok.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should be nil
lesson.amount_charged.should be 0.0
lesson.reload
user.reload
@ -131,29 +131,9 @@ describe "Monthly Recurring Lesson Flow" do
notification.description.should eql NotificationTypes::LESSON_MESSAGE
notification.message.should eql "Your lesson request is confirmed!"
# let user pay for it
LessonBooking.hourly_check
# teacher & student get into session
start = lesson_session.scheduled_start
end_time = lesson_session.scheduled_start + (60 * lesson_session.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
UserMailer.deliveries.clear
# background code comes around and analyses the session
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should be true
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson_session.billed.should be true
user.reload
user.lesson_purchases.length.should eql 1
lesson_purchase = user.lesson_purchases[0]
@ -174,11 +154,33 @@ describe "Monthly Recurring Lesson Flow" do
line_item.product_id.should eq LessonPackageType.single.id
line_item.lesson_package_purchase.should eql lesson_purchase
lesson_purchase.sale_line_item.should eql line_item
lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f
# teacher & student get into session
start = lesson_session.scheduled_start
end_time = lesson_session.scheduled_start + (60 * lesson_session.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
UserMailer.deliveries.clear
# background code comes around and analyses the session
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
if lesson_session.billing_error_detail
puts "monthly recurring lesson flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson.amount_charged.should eql 0.0
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be true
lesson_session.sent_billing_notices.should be false
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.length.should eql 2 # one for student, one for teacher
UserMailer.deliveries.length.should eql 1 # one for student
end
end

View File

@ -40,7 +40,7 @@ describe "Normal Lesson Flow" do
booking.card_presumed_ok.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should be nil
lesson.amount_charged.should be 0.0
lesson.reload
user.reload
@ -96,7 +96,7 @@ describe "Normal Lesson Flow" do
StripeMock.prepare_card_error(:card_declined)
lesson_session.billing_attempts.should eql 0
lesson_session.lesson_payment_charge.billing_attempts.should eql 0
user.lesson_purchases.length.should eql 0
LessonSession.hourly_check
lesson_session.reload
@ -104,7 +104,7 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should eql true
lesson_session.billing_attempts.should eql 1
puts lesson_session.billing_error_detail
lesson_session.billing_error_reason.should eql 'card_declined'
lesson_session.billed.should eql false
@ -120,7 +120,6 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should eql true
lesson_session.billing_error_reason.should eql 'card_declined'
lesson_session.billed.should eql false
lesson_session.billing_attempts.should eql 1
@ -137,7 +136,6 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should eql true
lesson_session.billing_attempts.should eql 2
lesson_session.billing_error_reason.should eql 'card_expired'
lesson_session.billed.should eql false
@ -154,7 +152,6 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should eql true
lesson_session.billing_attempts.should eql 3
lesson_session.billing_error_reason.should eql 'processing_error'
lesson_session.billed.should eql false
@ -171,8 +168,10 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should eql true
lesson_session.billing_attempts.should eql 4
if lesson_session.billing_error_detail
lesson_session.billing_error_detail
end
lesson_session.billing_error_reason.should eql 'processing_error'
lesson_session.billed.should eql true
user.reload
@ -198,6 +197,7 @@ describe "Normal Lesson Flow" do
line_item.product_id.should eq LessonPackageType.single.id
line_item.lesson_package_purchase.should eql lesson_purchase
lesson_purchase.sale_line_item.should eql line_item
lesson.reload
lesson.amount_charged.should eql (sale.recurly_total_in_cents / 100.0).to_f
lesson_session.billing_error_reason.should eql 'processing_error'
lesson_session.sent_billing_notices.should be true
@ -230,7 +230,7 @@ describe "Normal Lesson Flow" do
booking.card_presumed_ok.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should be nil
lesson.amount_charged.should eql 0.0
lesson.reload
user.reload
@ -340,7 +340,6 @@ describe "Normal Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should be true
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end

View File

@ -35,7 +35,7 @@ describe "Recurring Lesson Flow" do
booking.card_presumed_ok.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should be nil
lesson.amount_charged.should be 0.0
lesson.reload
user.reload
@ -149,7 +149,6 @@ describe "Recurring Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should be true
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end

View File

@ -23,10 +23,14 @@ describe "TestDrive Lesson Flow" do
booking.card_presumed_ok.should be_false
booking.should eql user.unprocessed_test_drive
booking.sent_notices.should be_false
user.reload
user.remaining_test_drives.should eql 0
########## Need validate their credit card
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', test_drive: true})
user.reload
user.remaining_test_drives.should eql 3
booking = result[:lesson]
lesson = booking.lesson_sessions[0]
test_drive = result[:test_drive]
@ -158,13 +162,12 @@ describe "TestDrive Lesson Flow" do
analysis = JSON.parse(lesson_session.analysis)
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.bill.should be false
lesson_session.billed.should be false
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be true
lesson_session.sent_notices.should be true
purchase = lesson_session.lesson_package_purchase
purchase.should_not be nil
purchase.price_in_cents.should eql 4999

View File

@ -0,0 +1,293 @@
require 'spec_helper'
describe TeacherPayment do
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
let(:teacher_obj) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account1_id)}
let(:teacher_obj2) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account2_id)}
let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) }
let(:teacher2) { FactoryGirl.create(:user, teacher: teacher_obj2) }
let(:test_drive_lesson) {testdrive_lesson(user, teacher)}
let(:test_drive_lesson2) {testdrive_lesson(user2, teacher2)}
let(:test_drive_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson, teacher: teacher, teacher_payment: nil, ready:false)}
let(:test_drive_distribution2) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson2, teacher: teacher2, teacher_payment: nil, ready:false)}
let(:normal_lesson_session) {normal_lesson(user, teacher)}
let(:normal_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: normal_lesson_session, teacher: teacher, teacher_payment: nil, ready:false)}
describe "pending_teacher_payments" do
it "empty" do
TeacherPayment.pending_teacher_payments.count.should eql 0
end
it "one distribution" do
test_drive_distribution.touch
payments = TeacherPayment.pending_teacher_payments
payments.count.should eql 0
test_drive_distribution.ready = true
test_drive_distribution.save!
payments = TeacherPayment.pending_teacher_payments
payments.count.should eql 1
payments[0]['id'].should eql teacher.id
end
it "multiple teachers" do
test_drive_distribution.touch
test_drive_distribution2.touch
payments = TeacherPayment.pending_teacher_payments
payments.count.should eql 0
test_drive_distribution.ready = true
test_drive_distribution.save!
test_drive_distribution2.ready = true
test_drive_distribution2.save!
payments = TeacherPayment.pending_teacher_payments
payments.count.should eql 2
payment_user_ids = payments.map(&:id)
payment_user_ids.include? teacher.id
payment_user_ids.include? teacher2.id
end
end
describe "teacher_payments" do
it "empty" do
TeacherPayment.teacher_payments
end
it "charges test drive" do
test_drive_distribution.touch
test_drive_distribution.ready = true
test_drive_distribution.save!
TeacherPayment.teacher_payments
test_drive_distribution.reload
test_drive_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 1
payment = test_drive_distribution.teacher_payment
if payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_detail
end
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 0
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
charge.application_fee.should eql nil
TeacherPayment.pending_teacher_payments.count.should eql 0
end
it "charges normal" do
normal_distribution.touch
normal_distribution.ready = true
normal_distribution.save!
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 1
payment = normal_distribution.teacher_payment
if payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_detail
end
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
charge.application_fee.should include("fee_")
end
it "charges multiple" do
test_drive_distribution.touch
test_drive_distribution.ready = true
test_drive_distribution.save!
normal_distribution.touch
normal_distribution.ready = true
normal_distribution.save!
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 2
payment = normal_distribution.teacher_payment
if payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_detail
end
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
charge.application_fee.should include("fee_")
test_drive_distribution.reload
payment = test_drive_distribution.teacher_payment
if payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_reason
puts payment.teacher_payment_charge.billing_error_detail
end
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 0
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
charge.application_fee.should be_nil
end
describe "stripe mocked" do
before { StripeMock.start }
after { StripeMock.stop; Timecop.return }
it "failed payment, then success" do
StripeMock.prepare_card_error(:card_declined)
normal_distribution.touch
normal_distribution.ready = true
normal_distribution.save!
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 1
payment = normal_distribution.teacher_payment
payment.teacher_payment_charge.billing_error_reason.should eql("card_declined")
payment.teacher_payment_charge.billing_error_detail.should include("declined")
payment.teacher_payment_charge.billed.should eql false
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
payment.teacher_payment_charge.stripe_charge_id.should be_nil
StripeMock.clear_errors
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 1
# make sure the teacher_payment is reused, and charge is reused
normal_distribution.teacher_payment.should eql(payment)
normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge)
# no attempt should be made because a day hasn't gone by
payment = normal_distribution.teacher_payment
payment.teacher_payment_charge.billed.should eql false
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
# advance one day so that a charge is attempted again
Timecop.freeze(Date.today + 2)
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 1
# make sure the teacher_payment is reused, and charge is reused
normal_distribution.teacher_payment.should eql(payment)
normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge)
# no attempt should be made because a day hasn't gone by
payment = normal_distribution.teacher_payment
payment.reload
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
end
it "charges multiple (with initial failure)" do
StripeMock.prepare_card_error(:card_declined)
test_drive_distribution.touch
test_drive_distribution.ready = true
test_drive_distribution.save!
normal_distribution.touch
normal_distribution.ready = true
normal_distribution.save!
TeacherPayment.teacher_payments
TeacherPayment.count.should eql 1
payment = TeacherPayment.first
payment.teacher_payment_charge.billed.should be_false
# advance one day so that a charge is attempted again
Timecop.freeze(Date.today + 2)
StripeMock.clear_errors
TeacherPayment.teacher_payments
normal_distribution.reload
normal_distribution.teacher_payment.should_not be_nil
TeacherPayment.count.should eql 2
payment = normal_distribution.teacher_payment
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 280
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
test_drive_distribution.reload
payment = test_drive_distribution.teacher_payment
payment.teacher_payment_charge.billed.should eql true
payment.teacher_payment_charge.amount_in_cents.should eql 1000
payment.teacher_payment_charge.fee_in_cents.should eql 0
teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.amount.should eql 1000
end
end
end
end

View File

@ -1,5 +1,5 @@
module Mock
module StripeMock
class ErrorQueue
def clear
@queue = []
@ -27,7 +27,7 @@ def testdrive_lesson(user, teacher, slots = nil)
booking = LessonBooking.book_test_drive(user, teacher, slots, "Hey I've heard of you before.")
puts "BOOKING #{booking.errors.inspect}"
#puts "BOOKING #{booking.errors.inspect}"
booking.errors.any?.should be_false
lesson = booking.lesson_sessions[0]
booking.card_presumed_ok.should be_true
@ -60,7 +60,7 @@ def normal_lesson(user, teacher, slots = nil)
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}"
# puts "NORMAL BOOKING #{booking.errors.inspect}"
booking.errors.any?.should be_false
lesson = booking.lesson_sessions[0]
booking.card_presumed_ok.should be_true

View File

@ -285,9 +285,18 @@ def app_config
def end_of_wait_window_forgiveness_minutes
1
end
def test_drive_wait_period_year
1
end
def stripe
{
:publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX',
:secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D',
:source_customer => 'cus_88Vp44SLnBWMXq' # seth@jamkazam.com in JamKazam-test account
}
end
private
def audiomixer_workspace_path
@ -300,7 +309,6 @@ def app_config
dev_path = "#{dev_path}/audiomixer/audiomixer/audiomixerapp"
dev_path if File.exist? dev_path
end
end
klass.new
@ -359,6 +367,46 @@ def friend(user1, user2)
FactoryGirl.create(:friendship, user: user2, friend: user1)
end
def stripe_oauth_client
# from here in the JamKazam Test account: https://dashboard.stripe.com/account/applications/settings
client_id = "ca_88T6HlHg1NRyKFossgWIz1Tf431Jshft"
options = {
:site => 'https://connect.stripe.com',
:authorize_url => '/oauth/authorize',
:token_url => '/oauth/token'
}
@stripe_oauth_client ||= OAuth2::Client.new(client_id, APP_CONFIG.stripe[:publishable_key], options)
end
def stripe_account2_id
# seth+stripe+test1@jamkazam.com / jam12345
"acct_17sCNpDcwjPgpqRL"
end
def stripe_account1_id
# seth+stripe1@jamkazam.com / jam123
=begin
curl -X POST https://connect.stripe.com/oauth/token \
-d client_secret=sk_test_OkjoIF7FmdjunyNsdVqJD02D \
-d code=ac_88U1TDwBgao4I3uYyyFEO3pVbbEed6tm \
-d grant_type=authorization_code
# {
"access_token": "sk_test_q8WZbdQXt7RGRkBR0fhgohG6",
"livemode": false,
"refresh_token": "rt_88U3csV42HtY1P1Cd9KU2GCez3wixgsHtIHaQbeeu1dXVWo9",
"token_type": "bearer",
"stripe_publishable_key": "pk_test_s1YZDczylyRUvhAGeVhxqznp",
"stripe_user_id": "acct_17sCNpDcwjPgpqRL",
"scope": "read_write"
}
=end
"acct_17sCEyH8FcKpNSnR"
end
def create_stripe_token(exp_month = 2017)
Stripe::Token.create(
:card => {

View File

@ -109,5 +109,11 @@ SampleApp::Application.configure do
config.video_available = "full"
config.guard_against_fraud = true
config.error_on_fraud = false
config.stripe = {
:publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX',
:secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D',
:source_customer => 'cus_88Vp44SLnBWMXq'
}
end