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