module JamRuby class Charge < ActiveRecord::Base attr_accessor :stripe_charge belongs_to :user, class_name: "JamRuby::User" validates :sent_billing_notices, inclusion: {in: [true, false]} def max_retries raise "not implemented" end 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 record_charge(stripe_charge) self.stripe_charge_id = stripe_charge.id self.billed = true self.billed_at = Time.now self.save(validate: false) end def charge(force = false) @stripe_charge = nil if !self.billed # check if we can bill at the moment if !force && last_billing_attempt_at && (charge_retry_hours.hours.ago < last_billing_attempt_at) return false end if !force && !billing_should_retry return 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 < max_retries self.last_billing_attempt_at = Time.now self.save(validate: false) begin stripe_charge = do_charge(force) # record the charge in this context (meaning, in our transaction) record_charge(@stripe_charge) if @stripe_charge rescue Stripe::StripeError => e stripe_handler(e) 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}).deliver do_send_unable_charge return false rescue Exception => e # record the charge even if there was an unhandled exception at some point record_charge(@stripe_charge) if @stripe_charge unhandled_handler(e) 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}).deliver 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 do_send_notices 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) end return stripe_charge end def unhandled_handler(e) self.billing_error_reason = e.to_s 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 #puts "Charge: unhandled exception #{billing_error_reason}, #{billing_error_detail}" 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 end end