require 'spec_helper' describe Account, "factories" do it "has a valid factory" do Factory(:account).should be_valid end it "has a valid paid factory" do Factory(:paid_account).should be_valid end end describe Account do subject { Factory(:account) } it "manifests braintree processor_declined errors as errors on number and doesn't save" do FakeBraintree.failures["4111111111111112"] = { "message" => "Do Not Honor", "code" => "2000", "status" => "processor_declined" } account = Factory.build(:paid_account, :card_number => "4111111111111112", :plan => Factory(:paid_plan)) account.save.should_not be FakeBraintree.customers.should be_empty account.persisted?.should_not be account.errors[:card_number].any? { |e| e =~ /denied/ }.should be end it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do FakeBraintree.failures["4111111111111112"] = { "message" => "Gateway Rejected: cvv", "code" => "N", "status" => "gateway_rejected" } account = Factory.build(:paid_account, :card_number => "4111111111111112", :plan => Factory(:paid_plan)) account.save.should_not be FakeBraintree.customers.should be_empty account.persisted?.should_not be account.errors[:verification_code].any? { |e| e =~ /did not match/ }.should be end it "manifests braintree gateway_rejected errors as errors on number and doesn't save" do FakeBraintree.failures["4111111111111112"] = { "message" => "Credit card number is invalid.", "errors" => { "customer" => { "errors" => [], "credit-card" => { "errors" => [{ "message" => "Credit card number is invalid.", "code" => 81715, "attribute" => :number }] }}}} account = Factory.build(:paid_account, :card_number => "4111111111111112", :plan => Factory(:paid_plan)) account.save.should_not be FakeBraintree.customers.should be_empty account.persisted?.should_not be account.errors[:card_number].any? { |e| e =~ /is invalid/ }.should be end end describe Account, "given free and paid plans" do let(:free) { Factory(:plan, :price => 0) } let(:paid) { Factory(:plan, :price => 1) } it "doesn't switch from free to paid without credit card info" do account = Factory(:account, :plan => free) account = Account.find(account.id) result = account.save_customer_and_subscription!(:plan_id => paid.id) result.should be_false account.reload.plan.should == free Saucy::Subscription::REQUIRED_CUSTOMER_ATTRIBUTES.each do |attribute| account.errors[attribute].should_not be_blank end FakeBraintree.customers[account.customer_token].should_not be_nil FakeBraintree.customers[account.customer_token]["credit_cards"].should be_blank end it "requires a billing email when upgrading to a paid plan" do account = Factory(:account, :plan => free, :card_number => '123') account.reload account.plan = paid account.should validate_presence_of(:billing_email) end it "requires a billing email for paid plans" do account = Factory.build(:account, :plan => paid, :card_number => '123') account.should validate_presence_of(:billing_email) end it "doesn't require a billing email for free plans" do account = Factory.build(:account, :plan => free) account.should_not validate_presence_of(:billing_email) end end describe Account, "with a paid plan" do subject do Factory(:paid_account, :plan => Factory(:paid_plan)) end it "has a customer_token" do subject.customer_token.should_not be_nil end it "has a subscription_token" do subject.subscription_token.should_not be_nil end it "has a customer" do subject.customer.should_not be_nil end it "has a credit card" do subject.credit_card.should_not be_nil end it "has a subscription" do subject.subscription.should_not be_nil end it "has a next_billing_date in the merchant account timezone" do Time.stubs(:now => Time.parse("2011-09-16 02:00 -0000")) subject.next_billing_date.should == Time.parse("Sun, 16 Oct 2011 00:00:00 EDT -04:00") end it "has an active subscription status" do subject.subscription_status.should == Braintree::Subscription::Status::Active end it "is not past due" do subject.past_due?.should_not be end it "creates a braintree customer, credit card, and subscription" do FakeBraintree.customers[subject.customer_token].should_not be_nil FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil end it "changes the subscription when the plan is changed" do new_plan = Factory(:paid_plan, :name => "New Plan", :price => 5) subject.save_customer_and_subscription!(:plan_id => new_plan.id) FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id FakeBraintree.subscriptions[subject.subscription_token]["price"].should == "5" end context "updating customer credit card information when changed" do before do subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com", :cardholder_name => "Jim Robot", :card_number => "4111111111111115", :verification_code => "123", :expiration_month => 5, :expiration_year => 2013) end it "updates the billing email" do subject.customer.email.should == "jrobot@example.com" end it "updates the cardholder name" do subject.credit_card.cardholder_name.should == "Jim Robot" end it "updates the card number" do subject.credit_card.last_4.should == "1115" end it "updates the expiration year" do subject.credit_card.expiration_year.should == 2013 end it "updates the expiration month" do subject.credit_card.expiration_month.should == 5 end end context "updating customer credit card billing address information when changed" do before do subject.save_customer_and_subscription!(:billing_email => "jrobot@example.com", :cardholder_name => "Jim Robot", :card_number => "4111111111111115", :verification_code => "123", :expiration_month => 5, :expiration_year => 2013, :street_address => "1 E Main St", :extended_address => "Suite 3", :locality => "Chicago", :region => "Illinois", :postal_code => "60622", :country_name => "United States of America") end it "updates the street address" do subject.billing_address.street_address.should == "1 E Main St" end it "updates the extended address" do subject.billing_address.extended_address.should == "Suite 3" end it "updates the locality" do subject.billing_address.locality.should == "Chicago" end it "updates the region" do subject.billing_address.region.should == "Illinois" end it "updates the postal code" do subject.billing_address.postal_code.should == "60622" end it "updates the country name" do subject.billing_address.country_name.should == "United States of America" end end it "deletes the customer when deleted" do subject.destroy FakeBraintree.customers[subject.customer_token].should be_nil end end describe Account, "with a free plan" do subject do Factory(:account, :plan => Factory(:plan)) end it "has a customer_token" do subject.customer_token.should_not be_nil end it "has a customer" do subject.customer.should_not be_nil end it "doesn't have a credit_card" do subject.credit_card.should be_nil end it "doesn't have a subscription_token" do subject.subscription_token.should be_nil end it "doesn't have a subscription" do subject.subscription.should be_nil end it "doesn't have a billing address" do subject.billing_address.should be_nil end it "creates a braintree customer" do FakeBraintree.customers[subject.customer_token].should_not be_nil end it "doesn't create a credit card, and subscription" do FakeBraintree.customers[subject.customer_token]["credit_cards"].should be_nil FakeBraintree.subscriptions[subject.subscription_token].should be_nil end it "creates a credit card, subscription, and billing address when the plan is changed to a paid plan and the billing info is supplied" do new_plan = Factory(:paid_plan, :name => "New Plan") subject.save_customer_and_subscription!(Factory.attributes_for(:paid_account, :plan_id => new_plan.id)) FakeBraintree.customers[subject.customer_token]["credit_cards"].first.should_not be_nil FakeBraintree.subscriptions[subject.subscription_token].should_not be_nil FakeBraintree.subscriptions[subject.subscription_token]["plan_id"].should == new_plan.id subject.credit_card.should_not be_nil subject.subscription.should_not be_nil subject.billing_address.should_not be_nil end it "passes up the merchant_account_id on the subscription when it's configured" do begin Saucy::Configuration.merchant_account_id = 'test' new_plan = Factory(:paid_plan, :name => "New Plan") subject.save_customer_and_subscription!(Factory.attributes_for(:paid_account, :plan_id => new_plan.id)) FakeBraintree.subscriptions[subject.subscription_token]["merchant_account_id"].should == 'test' ensure Saucy::Configuration.merchant_account_id = nil end end it "doesn't pass up the merchant_account_id on the subscription when it's not configured" do Saucy::Configuration.merchant_account_id = nil new_plan = Factory(:paid_plan, :name => "New Plan") subject.save_customer_and_subscription!(Factory.attributes_for(:paid_account, :plan_id => new_plan.id)) FakeBraintree.subscriptions[subject.subscription_token].keys.should_not include("merchant_account_id") end it "doesn't create a credit card, subscription, and billing address when the plan is changed to a different free plan" do new_plan = Factory(:plan, :name => "New Plan") subject.save_customer_and_subscription!(:plan_id => new_plan.id) subject.credit_card.should be_nil subject.subscription.should be_nil subject.billing_address.should be_nil end end describe Account, "with a plan and limits, and other plans" do subject { Factory(:account) } before do Factory(:limit, :name => "users", :value => 1, :plan => subject.plan) Factory(:limit, :name => "projects", :value => 1, :plan => subject.plan) Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => subject.plan) @can_switch = Factory(:plan) Factory(:limit, :name => "users", :value => 1, :plan => @can_switch) Factory(:limit, :name => "projects", :value => 1, :plan => @can_switch) Factory(:limit, :name => "ssl", :value => 0, :value_type => :boolean, :plan => @can_switch) @cannot_switch = Factory(:plan) Factory(:limit, :name => "users", :value => 0, :plan => @cannot_switch) Factory(:limit, :name => "projects", :value => 0, :plan => @cannot_switch) Factory(:limit, :name => "ssl", :value => 1, :value_type => :boolean, :plan => @cannot_switch) Factory(:membership, :account => subject) Factory(:project, :account => subject) end it "indicates whether the account can switch to another plan" do subject.can_change_plan_to?(@can_switch).should be subject.can_change_plan_to?(@cannot_switch).should_not be end end describe Account, "with a paid subscription" do subject do Factory(:paid_account, :plan => Factory(:paid_plan)) end let(:merchant_time_zone) { ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] } it "gets marked as past due and updates its next_billing_date when subscriptions are updated and it has been rejected by the gateway" do next_billing_date_string = 2.months.from_now.to_s(:braintree_date) subscription = FakeBraintree.subscriptions[subject.subscription_token] subscription["status"] = Braintree::Subscription::Status::PastDue subscription["next_billing_date"] = next_billing_date_string Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue subject.next_billing_date.should == merchant_time_zone.parse(next_billing_date_string) subject.past_due?.should be end end context "when there is an error" do before(:each) do Airbrake.stubs(:notify => true) @subscription = mock @subscription.stubs(:status).raises(RuntimeError) Account.any_instance.stubs(:subscription).returns(@subscription) end it "notifies Airbrake if expiring account notifications fail" do Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! Airbrake.should have_received(:notify).once() end end it "delivers the rest of the emails even if one fails" do Factory(:paid_account, :plan => Factory(:paid_plan)) Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! @subscription.should have_received(:status).twice() end end end context "when billing didn't work" do let(:subscription) { FakeBraintree.subscriptions[subject.subscription_token] } before do subscription["status"] = Braintree::Subscription::Status::PastDue subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date) FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed, :subscription_id => subject.subscription_token } subscription["transactions"] = [FakeBraintree.generated_transaction] end it "receives a receipt email at it's billing email with a notice that it failed when billing didn't work" do Timecop.travel(subject.next_billing_date + 1.day) do ActionMailer::Base.deliveries.clear Account.update_subscriptions! ActionMailer::Base.deliveries.any? do |email| email.to == [subject.billing_email] && email.subject =~ /problem/i end.should be end end it "does not notify observers of billing when billing didn't work" do Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! should_not notify_observers("billed", :account => subject) end end end context "with multiple historical transactions and an active account" do let(:subscription) { FakeBraintree.subscriptions[subject.subscription_token] } let!(:earlier_transaction_date) { 2.months.ago } let!(:later_transaction_date) { 1.month.ago } def build_transaction(attributes) common_transaction_attributes = { :status => Braintree::Transaction::Status::Settled, :subscription_id => subject.subscription_token } FakeBraintree.transaction = common_transaction_attributes.merge(attributes) FakeBraintree.generated_transaction end before do subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date) subscription["status"] = Braintree::Subscription::Status::Active subscription["transactions"] = [ build_transaction(:created_at => later_transaction_date), build_transaction(:created_at => earlier_transaction_date) ] it "receives a receipt email with the correct transaction for active accounts" do Timecop.travel(subject.next_billing_date + 1.day) do ActionMailer::Base.deliveries.clear Account.update_subscriptions! ActionMailer::Base.deliveries.any? do |email| email.subject =~ /receipt/i && email.to_s.include?(later_transaction_date.strftime("%x")) end.should be end end end end context "with a merchant account timezone different from the system timezone" do def build_subscription(account) subscription = FakeBraintree.subscriptions[account.subscription_token] subscription["status"] = Braintree::Subscription::Status::Active subscription["transactions"] = [FakeBraintree.generated_transaction] subscription end before do @old_env_tz = ENV['TZ'] ENV['TZ'] = "Europe/Paris" Saucy::Configuration.merchant_account_time_zone = "Eastern Time (US & Canada)" subject.next_billing_date = "2011-08-15" subject.save! subscription = build_subscription(subject) subscription["next_billing_date"] = "2011-09-15" Time.stubs(:now => Time.parse("2011-08-16 02:00 -0000")) Account.update_subscriptions! ActionMailer::Base.deliveries.clear subject.reload.next_billing_date.should == Time.parse("2011-09-15 00:00 -0400") end after do ENV['TZ'] = @old_env_tz end it "does not update accounts if their billing date hasn't elapsed from the merchant account's perspective" do Time.stubs(:now => Time.parse("2011-09-15 02:00 -0000")) Account.update_subscriptions! ActionMailer::Base.deliveries.should be_empty end it "does update accounts if their billing date has elapsed from the merchant account's perspective" do Time.stubs(:now => Time.parse("2011-09-16 02:00 -0000")) Account.update_subscriptions! ActionMailer::Base.deliveries.should_not be_empty end end context "and an active subscription due 2 months from now" do let(:subscription) { FakeBraintree.subscriptions[subject.subscription_token] } let(:next_billing_date_string) { 2.months.from_now.to_s(:braintree_date) } let(:zone) { ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] } before do subscription["status"] = Braintree::Subscription::Status::Active subscription["next_billing_date"] = next_billing_date_string FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled, :subscription_id => subject.subscription_token } subscription["transactions"] = [FakeBraintree.generated_transaction] end it "gets marked as not past due and updates its next_billing_date when the subscription is active after its billing date" do Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! subject.reload.subscription_status.should == Braintree::Subscription::Status::Active subject.next_billing_date.should == zone.parse(next_billing_date_string) end end it "receives a receipt email at it's billing email with transaction details" do Timecop.travel(subject.next_billing_date + 1.day) do ActionMailer::Base.deliveries.clear Account.update_subscriptions! ActionMailer::Base.deliveries.any? do |email| email.to == [subject.billing_email] && email.subject =~ /receipt/i end.should be end end it "notifies observers of billing" do Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! should notify_observers("billed", :account => subject) end end it "doesn't receive a receipt email when it's already been billed" do Timecop.travel(subject.next_billing_date - 1.day) do ActionMailer::Base.deliveries.clear Account.update_subscriptions! ActionMailer::Base.deliveries.select do |email| email.to == [subject.billing_email] && email.subject =~ /receipt/i end.should be_empty end end it "does not notify observers of billing when it's already been billed" do Timecop.travel(subject.next_billing_date - 1.day) do Account.update_subscriptions! should_not notify_observers("billed", :account => subject) end end end end describe Account, "with a paid subscription that is past due" do subject do Factory(:paid_account, :plan => Factory(:paid_plan)) end before do subscription = FakeBraintree.subscriptions[subject.subscription_token] subscription["status"] = Braintree::Subscription::Status::PastDue subscription["next_billing_date"] = 2.months.from_now.to_s(:braintree_date) Timecop.travel(subject.next_billing_date + 1.day) do Account.update_subscriptions! end subject.reload end let(:zone) { ActiveSupport::TimeZone[Saucy::Configuration.merchant_account_time_zone] } it "retries the subscription charge and updates the subscription when the billing information is correctly updated" do next_billing_date_string = 1.day.from_now.to_s(:braintree_date) subscription = FakeBraintree.subscriptions[subject.subscription_token] subscription["status"] = Braintree::Subscription::Status::Active subscription["next_billing_date"] = next_billing_date_string FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled, :subscription_id => subject.subscription_token } transaction = FakeBraintree.generated_transaction subscription["transactions"] = [transaction] retry_transaction = stub(:id => "12345") retry_authorization = stub(:transaction => retry_transaction) Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization) Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => true)) subject.save_customer_and_subscription!(:card_number => "4111111111111111", :verification_code => "124", :expiration_month => 6, :expiration_year => 2012).should be subject.reload.subscription_status.should == Braintree::Subscription::Status::Active subject.next_billing_date.should == zone.parse(next_billing_date_string) end it "retries the subscription charge and updates the subscription when the payment processing fails" do next_billing_date_string = 2.months.from_now.to_s(:braintree_date) subscription = FakeBraintree.subscriptions[subject.subscription_token] subscription["status"] = Braintree::Subscription::Status::PastDue subscription["next_billing_date"] = next_billing_date_string FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed, :subscription_id => subject.subscription_token } transaction = FakeBraintree.generated_transaction subscription["transactions"] = [transaction] retry_transaction = stub(:id => "12345", :status => "processor_declined", "processor_response_text" => "no good") retry_authorization = stub(:transaction => retry_transaction) Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization) Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => [])) subject.save_customer_and_subscription!(:card_number => "4111111111", :verification_code => "124", :expiration_month => 6, :expiration_year => 2012).should_not be subject.errors[:card_number].should include("was denied by the payment processor with the message: no good") subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue subject.next_billing_date.should == zone.parse(next_billing_date_string) end it "retries the subscription charge and updates the subscription when the settlement fails" do next_billing_date_string = 1.day.from_now.to_s(:braintree_date) subscription = FakeBraintree.subscriptions[subject.subscription_token] subscription["status"] = Braintree::Subscription::Status::PastDue subscription["next_billing_date"] = next_billing_date_string FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed, :subscription_id => subject.subscription_token } transaction = FakeBraintree.generated_transaction subscription["transactions"] = [transaction] retry_transaction = stub(:id => "12345", :status => "") retry_authorization = stub(:transaction => retry_transaction) Braintree::Subscription.expects(:retry_charge).with(subject.subscription_token).returns(retry_authorization) Braintree::Transaction.expects(:submit_for_settlement).with(retry_transaction.id).returns(stub(:success? => false, :errors => [stub(:attribute => 'number', :message => 'no good')])) subject.save_customer_and_subscription!(:card_number => "4111111111", :verification_code => "124", :expiration_month => 6, :expiration_year => 2012).should_not be subject.errors[:card_number].should include("no good") subject.reload.subscription_status.should == Braintree::Subscription::Status::PastDue subject.next_billing_date.should == zone.parse(next_billing_date_string) end end describe Account, "that is activated" do subject do Factory(:paid_account, :plan => Factory(:paid_plan)) end it "notifies observers of activations" do subject.activated = true subject.save! should notify_observers("activated", :account => subject) end it "notifies observers of activation only once" do 2.times do subject.activated = true subject.save! end should notify_observers_once("activated", :account => subject) end end