diff --git a/db/up/lessons.sql b/db/up/lessons.sql
index 11f010ecf..e5e5b7b9c 100644
--- a/db/up/lessons.sql
+++ b/db/up/lessons.sql
@@ -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
-);
\ No newline at end of file
+);
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 9b5f099d4..f929b7b71 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
index 509d5ba28..755385bcc 100644
--- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
+++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb
new file mode 100644
index 000000000..96bf19889
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards,
Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb
new file mode 100644
index 000000000..96bf19889
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards,
Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb
new file mode 100644
index 000000000..3dc58d8ec
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+ You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
+
+
+ Click the button below to see more information about this session.
+
+
+ VIEW
+ LESSON DETAILS
+
+
+ Best Regards,
Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb
new file mode 100644
index 000000000..f21f164b5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb
@@ -0,0 +1,5 @@
+Hello <%= @student.name %>,
+
+You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb
new file mode 100644
index 000000000..af2681325
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb
@@ -0,0 +1,41 @@
+<% provide(:title, @subject) %>
+
+You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:
+
+
+<% @teacher_payment.teacher_distributions.each do |distribution| %>
+
+ <% if distribution.is_test_drive? %>
+ You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
+ <% elsif distribution.is_normal? %>
+ You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
+ <% elsif distribution.is_monthly? %>
+ You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
+ <% else %>
+ Unknown payment type.
+ <% end %>
+
+
+
+<% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb
new file mode 100644
index 000000000..2e18fa163
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb
@@ -0,0 +1,31 @@
+<% provide(:title, @subject) %>
+
+You were paid a total of $<%= @teacher_payment.amount %> for your participation in JamClass. Below are more details:
+
+<% @teacher_payment.teacher_distributions.each do |distribution| %>
+
+<% if distribution.is_test_drive? %>
+You have earned $<%= distribution.amount %> for your TestDrive lesson with <%= distribution.student.name %>.
+<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% elsif distribution.is_normal? %>
+You have earned $<%= distribution.amount %> for your lesson with <%= distribution.student.name %>.
+<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% elsif distribution.is_monthly? %>
+You have earned $<%= distribution.amount %> for your <%= distribution.month_name%> lesson with <%= distribution.student.name %>.
+<% if !@teacher_payment.teacher.has_rated_student(distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% else %>
+Unknown payment type.
+<% end %>
+<% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb
new file mode 100644
index 000000000..bdd291b49
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb
@@ -0,0 +1,15 @@
+<% provide(:title, @subject) %>
+
+
+ <% if @card_declined %>
+ When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
+ <% elsif @card_expired %>
+ When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
+ <% else %>
+ For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
+ <% end %>
+
+
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb
new file mode 100644
index 000000000..8cc72bce5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb
@@ -0,0 +1,12 @@
+<% provide(:title, @subject) %>
+
+ <% if @card_declined %>
+When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
+ <% elsif @card_expired %>
+When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
+ <% else %>
+For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
+ <% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb
index e88aed9c7..e13f5bf9e 100644
--- a/ruby/lib/jam_ruby/models/affiliate_partner.rb
+++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb
new file mode 100644
index 000000000..7955fc3a1
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/affiliate_payment_charge.rb
@@ -0,0 +1,51 @@
+module JamRuby
+ class AffiliatePaymentCharge < Charge
+
+ has_one :teacher_payment, class_name: "JamRuby::TeacherPayment", foreign_key: :affiliate_charge_id
+
+ def distribution
+ @distribution ||= teacher_payment.teacher_distribution
+ end
+
+ def max_retries
+ 9999999
+ end
+
+ def teacher
+ @teacher ||= teacher_payment.teacher
+ end
+
+ def charged_user
+ teacher
+ end
+
+ def do_charge
+
+ # source will let you supply a token. But... how to get a token in this case?
+
+ stripe_charge = Stripe::Charge.create(
+ :amount => amount_in_cents,
+ :currency => "usd",
+ :customer => APP_CONFIG.stripe[:source_customer],
+ :description => construct_description,
+ :destination => teacher.teacher.stripe_account_id,
+ :application_fee => fee_in_cents,
+ )
+
+ stripe_charge
+ end
+
+ def do_send_notices
+ UserMailer.teacher_distribution_done(teacher_payment)
+ end
+
+ def do_send_unable_charge
+ UserMailer.teacher_distribution_fail(teacher_payment)
+ end
+
+ def construct_description
+ teacher_payment.teacher_distribution.description
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/charge.rb b/ruby/lib/jam_ruby/models/charge.rb
index 26279c64b..57c0fe3b4 100644
--- a/ruby/lib/jam_ruby/models/charge.rb
+++ b/ruby/lib/jam_ruby/models/charge.rb
@@ -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 student’s 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')
diff --git a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
index b6c1d94ee..d09f67670 100644
--- a/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
+++ b/ruby/lib/jam_ruby/models/lesson_package_purchase.rb
@@ -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 student’s 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
+
diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb
index 2c17e1235..d60aaa14a 100644
--- a/ruby/lib/jam_ruby/models/lesson_package_type.rb
+++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/lesson_payment_charge.rb b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb
new file mode 100644
index 000000000..cd8ea3abf
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/lesson_payment_charge.rb
@@ -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
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb
index b6c6c7db7..ba4e64103 100644
--- a/ruby/lib/jam_ruby/models/lesson_session.rb
+++ b/ruby/lib/jam_ruby/models/lesson_session.rb
@@ -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 student’s 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
diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb
index 76d25ecb8..3c78c8082 100644
--- a/ruby/lib/jam_ruby/models/sale.rb
+++ b/ruby/lib/jam_ruby/models/sale.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb
index 9a9fcc93d..e1f02d239 100644
--- a/ruby/lib/jam_ruby/models/teacher.rb
+++ b/ruby/lib/jam_ruby/models/teacher.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb
index 266fdfe38..5bed06c3b 100644
--- a/ruby/lib/jam_ruby/models/teacher_distribution.rb
+++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb
@@ -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
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/teacher_payment.rb b/ruby/lib/jam_ruby/models/teacher_payment.rb
new file mode 100644
index 000000000..1076c4a70
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/teacher_payment.rb
@@ -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
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb
index 276d5ab42..e54bc5330 100644
--- a/ruby/lib/jam_ruby/models/teacher_payment_charge.rb
+++ b/ruby/lib/jam_ruby/models/teacher_payment_charge.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 9dd5cfa23..a287cf412 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb
index 8dd28d3b6..623ea5378 100644
--- a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb
+++ b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb
@@ -8,6 +8,8 @@ module JamRuby
def self.perform
@@log.debug("waking up")
+ TeacherPayment.daily_check
+
bounced_emails
calendar_manager = CalendarManager.new
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index dcb113cf5..4a694e6dd 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -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'
diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb
index f4b2acb84..6ede6439c 100644
--- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb
+++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb
index ce0b7b800..36cecc05b 100644
--- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb
+++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb
index 18d2107ef..511ac542b 100644
--- a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb
+++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb
index a86c4c9bd..ee42325f5 100644
--- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb
+++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/models/teacher_payment_spec.rb b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb
new file mode 100644
index 000000000..58ac4d16f
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/teacher_payment_spec.rb
@@ -0,0 +1,293 @@
+require 'spec_helper'
+
+describe TeacherPayment do
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+ let(:teacher_obj) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account1_id)}
+ let(:teacher_obj2) {FactoryGirl.create(:teacher, stripe_account_id: stripe_account2_id)}
+ let(:teacher) { FactoryGirl.create(:user, teacher: teacher_obj) }
+ let(:teacher2) { FactoryGirl.create(:user, teacher: teacher_obj2) }
+ let(:test_drive_lesson) {testdrive_lesson(user, teacher)}
+ let(:test_drive_lesson2) {testdrive_lesson(user2, teacher2)}
+ let(:test_drive_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson, teacher: teacher, teacher_payment: nil, ready:false)}
+ let(:test_drive_distribution2) {FactoryGirl.create(:teacher_distribution, lesson_session: test_drive_lesson2, teacher: teacher2, teacher_payment: nil, ready:false)}
+ let(:normal_lesson_session) {normal_lesson(user, teacher)}
+ let(:normal_distribution) {FactoryGirl.create(:teacher_distribution, lesson_session: normal_lesson_session, teacher: teacher, teacher_payment: nil, ready:false)}
+
+ describe "pending_teacher_payments" do
+
+ it "empty" do
+ TeacherPayment.pending_teacher_payments.count.should eql 0
+ end
+
+ it "one distribution" do
+ test_drive_distribution.touch
+
+ payments = TeacherPayment.pending_teacher_payments
+ payments.count.should eql 0
+
+ test_drive_distribution.ready = true
+ test_drive_distribution.save!
+
+ payments = TeacherPayment.pending_teacher_payments
+ payments.count.should eql 1
+ payments[0]['id'].should eql teacher.id
+ end
+
+ it "multiple teachers" do
+ test_drive_distribution.touch
+ test_drive_distribution2.touch
+
+ payments = TeacherPayment.pending_teacher_payments
+ payments.count.should eql 0
+
+ test_drive_distribution.ready = true
+ test_drive_distribution.save!
+ test_drive_distribution2.ready = true
+ test_drive_distribution2.save!
+
+ payments = TeacherPayment.pending_teacher_payments
+ payments.count.should eql 2
+ payment_user_ids = payments.map(&:id)
+ payment_user_ids.include? teacher.id
+ payment_user_ids.include? teacher2.id
+ end
+ end
+
+ describe "teacher_payments" do
+ it "empty" do
+ TeacherPayment.teacher_payments
+ end
+
+ it "charges test drive" do
+ test_drive_distribution.touch
+
+ test_drive_distribution.ready = true
+ test_drive_distribution.save!
+
+ TeacherPayment.teacher_payments
+
+ test_drive_distribution.reload
+ test_drive_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 1
+
+ payment = test_drive_distribution.teacher_payment
+
+ if payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_detail
+ end
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 0
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ charge.application_fee.should eql nil
+
+ TeacherPayment.pending_teacher_payments.count.should eql 0
+ end
+
+ it "charges normal" do
+ normal_distribution.touch
+
+ normal_distribution.ready = true
+ normal_distribution.save!
+
+ TeacherPayment.teacher_payments
+
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 1
+
+ payment = normal_distribution.teacher_payment
+
+ if payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_detail
+ end
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ charge.application_fee.should include("fee_")
+ end
+
+ it "charges multiple" do
+ test_drive_distribution.touch
+ test_drive_distribution.ready = true
+ test_drive_distribution.save!
+ normal_distribution.touch
+ normal_distribution.ready = true
+ normal_distribution.save!
+
+ TeacherPayment.teacher_payments
+
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 2
+
+ payment = normal_distribution.teacher_payment
+
+ if payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_detail
+ end
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ charge.application_fee.should include("fee_")
+
+ test_drive_distribution.reload
+ payment = test_drive_distribution.teacher_payment
+
+ if payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_reason
+ puts payment.teacher_payment_charge.billing_error_detail
+ end
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 0
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ charge.application_fee.should be_nil
+ end
+
+
+
+ describe "stripe mocked" do
+ before { StripeMock.start }
+ after { StripeMock.stop; Timecop.return }
+
+ it "failed payment, then success" do
+ StripeMock.prepare_card_error(:card_declined)
+
+ normal_distribution.touch
+ normal_distribution.ready = true
+ normal_distribution.save!
+
+ TeacherPayment.teacher_payments
+
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 1
+
+ payment = normal_distribution.teacher_payment
+
+ payment.teacher_payment_charge.billing_error_reason.should eql("card_declined")
+ payment.teacher_payment_charge.billing_error_detail.should include("declined")
+
+ payment.teacher_payment_charge.billed.should eql false
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+
+ payment.teacher_payment_charge.stripe_charge_id.should be_nil
+
+ StripeMock.clear_errors
+
+ TeacherPayment.teacher_payments
+
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 1
+
+ # make sure the teacher_payment is reused, and charge is reused
+ normal_distribution.teacher_payment.should eql(payment)
+ normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge)
+
+ # no attempt should be made because a day hasn't gone by
+ payment = normal_distribution.teacher_payment
+ payment.teacher_payment_charge.billed.should eql false
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+
+ # advance one day so that a charge is attempted again
+ Timecop.freeze(Date.today + 2)
+
+ TeacherPayment.teacher_payments
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 1
+
+ # make sure the teacher_payment is reused, and charge is reused
+ normal_distribution.teacher_payment.should eql(payment)
+ normal_distribution.teacher_payment.teacher_payment_charge.should eql(payment.teacher_payment_charge)
+
+ # no attempt should be made because a day hasn't gone by
+ payment = normal_distribution.teacher_payment
+ payment.reload
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ end
+
+ it "charges multiple (with initial failure)" do
+ StripeMock.prepare_card_error(:card_declined)
+
+ test_drive_distribution.touch
+ test_drive_distribution.ready = true
+ test_drive_distribution.save!
+ normal_distribution.touch
+ normal_distribution.ready = true
+ normal_distribution.save!
+
+ TeacherPayment.teacher_payments
+
+ TeacherPayment.count.should eql 1
+ payment = TeacherPayment.first
+ payment.teacher_payment_charge.billed.should be_false
+
+ # advance one day so that a charge is attempted again
+ Timecop.freeze(Date.today + 2)
+
+ StripeMock.clear_errors
+ TeacherPayment.teacher_payments
+
+ normal_distribution.reload
+ normal_distribution.teacher_payment.should_not be_nil
+ TeacherPayment.count.should eql 2
+
+ payment = normal_distribution.teacher_payment
+
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 280
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+
+ test_drive_distribution.reload
+ payment = test_drive_distribution.teacher_payment
+
+ payment.teacher_payment_charge.billed.should eql true
+ payment.teacher_payment_charge.amount_in_cents.should eql 1000
+ payment.teacher_payment_charge.fee_in_cents.should eql 0
+ teacher_distribution = payment.teacher_payment_charge.distribution
+ teacher_distribution.amount_in_cents.should eql 1000
+ charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
+ charge.amount.should eql 1000
+ end
+ end
+
+ end
+end
diff --git a/ruby/spec/support/lesson_session.rb b/ruby/spec/support/lesson_session.rb
index 18d4be033..401dad8f4 100644
--- a/ruby/spec/support/lesson_session.rb
+++ b/ruby/spec/support/lesson_session.rb
@@ -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
diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb
index eeca82102..ecd19635b 100644
--- a/ruby/spec/support/utilities.rb
+++ b/ruby/spec/support/utilities.rb
@@ -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 => {
diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb
index a5b075118..c6d7e3d00 100644
--- a/web/config/environments/test.rb
+++ b/web/config/environments/test.rb
@@ -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