require 'airbrake' module Saucy module Subscription extend ActiveSupport::Concern included do require "rubygems" require "braintree" extend ActiveSupport::Memoizable CUSTOMER_ATTRIBUTES = [ :cardholder_name, :billing_email, :card_number, :expiration_month, :expiration_year, :verification_code, :street_address, :extended_address, :locality, :region, :postal_code, :country_name ] OPTIONAL_CUSTOMER_ATTRIBUTES = [:extended_address] REQUIRED_CUSTOMER_ATTRIBUTES = CUSTOMER_ATTRIBUTES - OPTIONAL_CUSTOMER_ATTRIBUTES attr_accessor *CUSTOMER_ATTRIBUTES REQUIRED_CUSTOMER_ATTRIBUTES.each do |attribute| validates_presence_of attribute, :if => :switching_to_billed? end before_create :create_customer before_create :create_subscription, :if => :billed? after_destroy :destroy_customer memoize :customer memoize :subscription end module InstanceMethods def customer Braintree::Customer.find(customer_token) if customer_token end def credit_card customer.credit_cards[0] if customer && customer.credit_cards.any? end def billing_address credit_card.billing_address if credit_card end def subscription Braintree::Subscription.find(subscription_token) if subscription_token end def save_customer_and_subscription!(attributes) successful = true self.plan = ::Plan.find(attributes[:plan_id]) if changing_plan?(attributes) if changing_customer_attributes?(attributes) successful = update_customer(attributes) end if successful && past_due? successful = retry_subscription_charge! end if successful && changing_plan?(attributes) save_subscription flush_cache :subscription end successful && save end def can_change_plan_to?(new_plan) within_limits_for?(new_plan) && !past_trial_for?(new_plan) end def past_due? subscription_status == Braintree::Subscription::Status::PastDue end def most_recent_transaction subscription.transactions.sort_by(&:created_at).last end private def within_limits_for?(new_plan) new_plan.limits.where(:value_type => :number).all? do |limit| new_plan.limit(limit.name).value >= send(:"#{limit.name}_count") end end def past_trial_for?(new_plan) new_plan.trial? && past_trial? end def retry_subscription_charge! authorized_transaction = Braintree::Subscription.retry_charge(subscription.id).transaction result = Braintree::Transaction.submit_for_settlement(authorized_transaction.id) handle_errors(authorized_transaction, result.errors) if !result.success? update_subscription_cache! result.success? end def update_subscription_cache! zone = ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] flush_cache :subscription update_attribute(:subscription_status, subscription.status) update_attribute(:next_billing_date, zone.parse(subscription.next_billing_date)) end def changing_plan?(attributes) attributes[:plan_id].present? end def changing_customer_attributes?(attributes) CUSTOMER_ATTRIBUTES.any? { |attribute| attributes[attribute].present? } end def set_customer_attributes(attributes) CUSTOMER_ATTRIBUTES.each do |attribute| send("#{attribute}=", attributes[attribute]) if attributes[attribute].present? end end def update_customer(attributes) set_customer_attributes(attributes) if valid? result = Braintree::Customer.update(customer_token, customer_attributes) handle_customer_result(result) if !result.success? Airbrake.notify(nil, :error_class => "Customer update", :error_message => "Customer update failed", :parameters => { :customer_token => customer_token, :customer_attributes => customer_attributes } ) end result.success? else Airbrake.notify(nil, :error_class => "Subscription Validation", :error_message => "Customer failed validation", :parameters => customer_attributes ) end end def save_subscription if subscription Braintree::Subscription.update(subscription_token, :plan_id => plan_id, :price => plan.price.to_s) elsif plan.billed? valid? && create_subscription end end def customer_attributes { :email => billing_email, :credit_card => credit_card_attributes } end def credit_card_attributes if plan.billed? card_attributes = { :cardholder_name => cardholder_name, :number => card_number, :expiration_month => expiration_month, :expiration_year => expiration_year, :cvv => verification_code, :billing_address => { :street_address => street_address, :extended_address => extended_address, :locality => locality, :region => region, :postal_code => postal_code, :country_name => country_name } } if credit_card card_attributes.merge!(:options => credit_card_options) end card_attributes else {} end end def credit_card_options if customer && customer.credit_cards.any? { :update_existing_token => credit_card.token } else {} end end def create_customer result = Braintree::Customer.create(customer_attributes) handle_customer_result(result) end def destroy_customer Braintree::Customer.delete(customer_token) end def handle_customer_result(result) if result.success? self.customer_token = result.customer.id flush_cache :customer else handle_errors(result.credit_card_verification, result.errors) end result.success? end def handle_errors(result, remote_errors) Airbrake.notify(nil, :error_class => "handle_errors", :error_message => "handle_errors", :parameters => { :result => result, :remote_errors => remote_errors } ) if result && result.status == "processor_declined" errors[:card_number] << "was denied by the payment processor with the message: #{result.processor_response_text}" elsif result && result.status == "gateway_rejected" errors[:verification_code] << "did not match" elsif remote_errors.any? remote_errors.each do |error| if error.attribute == "number" errors[:card_number] << error.message.gsub("Credit card number ", "") elsif error.attribute == "CVV" errors[:verification_code] << error.message.gsub("CVV ", "") elsif error.attribute == "expiration_month" errors[:expiration_month] << error.message.gsub("Expiration month ", "") elsif error.attribute == "expiration_year" errors[:expiration_year] << error.message.gsub("Expiration year ", "") end end end end def create_subscription result = Braintree::Subscription.create(subscription_attributes) if result.success? zone = ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] self.subscription_token = result.subscription.id self.next_billing_date = zone.parse(result.subscription.next_billing_date) self.subscription_status = result.subscription.status else Airbrake.notify(nil, :error_class => "Subscription creation", :error_message => "Subscription creation failed", :parameters => subscription_attributes ) false end end def subscription_attributes { :payment_method_token => credit_card.token, :plan_id => plan_id, :merchant_account_id => Saucy::Configuration.merchant_account_id }.tap do |attributes| attributes.reject! { |key, value| value.nil? } end end def switching_to_billed? plan_id && plan.billed? && subscription_token.blank? end end module ClassMethods def update_subscriptions! recently_billed = where("next_billing_date <= ?", Time.now) recently_billed.each do |account| begin zone = ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] account.subscription_status = account.subscription.status account.next_billing_date = zone.parse(account.subscription.next_billing_date) account.save! if account.past_due? BillingMailer.problem(account).deliver! else BillingMailer.receipt(account, account.most_recent_transaction).deliver! Saucy::Notifications.notify_observers("billed", :account => account) end rescue Exception => e Airbrake.notify(e) end end end end end end