Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Imported the subscription and error models

  • Loading branch information...
commit 31ce31b5c283ca1ea44d1a33b0c7cf7e808d9a67 1 parent c36871b
@jferris jferris authored
View
2  .rspec
@@ -0,0 +1,2 @@
+--color
+--format documentation
View
7 Rakefile
@@ -1 +1,8 @@
require "bundler/gem_tasks"
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new do |t|
+ t.pattern = 'spec/**/{*_spec.rb}'
+end
+
+task :default => [:spec]
View
13 ashmont.gemspec
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
s.authors = ["thoughtbot"]
s.email = ["jferris@thoughtbot.com"]
s.homepage = ""
- s.summary = %q{TODO: Write a gem summary}
- s.description = %q{TODO: Write a gem description}
+ s.summary = %q{ActiveModel-like objects and helpers for interacting with Braintree.}
+ s.description = %q{ActiveModel-like objects and helpers for interacting with Braintree.}
s.rubyforge_project = "ashmont"
@@ -17,4 +17,13 @@ Gem::Specification.new do |s|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
+
+ s.add_dependency('braintree', '>= 2.6.2')
+ s.add_dependency('activesupport', '>= 3.0.0')
+ s.add_dependency('i18n', '>= 0.6')
+ s.add_dependency('tzinfo', '>= 0.3')
+ s.add_development_dependency('bourne')
+ s.add_development_dependency('rake')
+ s.add_development_dependency('rspec')
+ s.add_development_dependency('timecop')
end
View
9 lib/ashmont.rb
@@ -1 +1,10 @@
require "ashmont/version"
+require "ashmont/subscription"
+
+module Ashmont
+ class << self
+ attr_accessor :merchant_account_time_zone
+ end
+
+ self.merchant_account_time_zone = 'Eastern Time (US & Canada)'
+end
View
45 lib/ashmont/errors.rb
@@ -0,0 +1,45 @@
+module Ashmont
+ class Errors
+ ERROR_MESSAGE_PREFIXES = {
+ "number" => "Credit card number ",
+ "CVV" => "CVV ",
+ "expiration_month" => "Expiration month ",
+ "expiration_year" => "Expiration year "
+ }
+
+ def initialize(result, remote_errors)
+ @errors = {}
+ parse_result(result)
+ parse_remote_errors(remote_errors)
+ end
+
+ def to_hash
+ @errors.dup
+ end
+
+ private
+
+ def parse_result(result)
+ case result.status
+ when "processor_declined"
+ add_error :number, "was denied by the payment processor with the message: #{result.processor_response_text}"
+ when "gateway_rejected"
+ add_error :cvv, "did not match"
+ end
+ end
+
+ def parse_remote_errors(remote_errors)
+ remote_errors.each do |error|
+ if prefix = ERROR_MESSAGE_PREFIXES[error.attribute]
+ message = error.message.sub(prefix, "")
+ add_error error.attribute.downcase, message
+ end
+ end
+ end
+
+ def add_error(attribute, message)
+ @errors[attribute] ||= []
+ @errors[attribute] << message
+ end
+ end
+end
View
75 lib/ashmont/subscription.rb
@@ -0,0 +1,75 @@
+require "ashmont/errors"
+require "active_support/core_ext"
+require "braintree"
+
+module Ashmont
+ class Subscription
+ attr_reader :token, :errors
+
+ def initialize(attributes = {})
+ @attributes = attributes.except(:token)
+ @token = attributes[:token]
+ @errors = {}
+ end
+
+ delegate :transactions, :status, :to => :remote_subscription
+
+ def save
+ if token
+ update
+ else
+ create
+ end
+ end
+
+ def retry_charge
+ transaction = Braintree::Subscription.retry_charge(token).transaction
+ result = Braintree::Transaction.submit_for_settlement(transaction.id)
+ if result.success?
+ true
+ else
+ @errors = Ashmont::Errors.new(transaction, result.errors)
+ false
+ end
+ end
+
+ def most_recent_transaction
+ transactions.sort_by(&:created_at).last
+ end
+
+ def next_billing_date
+ merchant_account_time_zone.parse(remote_subscription.next_billing_date)
+ end
+
+ def reload
+ @remote_subscription = nil
+ self
+ end
+
+ private
+
+ def create
+ result = Braintree::Subscription.create(@attributes)
+ if result.success?
+ @token = result.subscription.id
+ @remote_subscription = result.subscription
+ true
+ else
+ false
+ end
+ end
+
+ def update
+ @remote_subscription = nil
+ Braintree::Subscription.update(token, @attributes)
+ end
+
+ def remote_subscription
+ @remote_subscription ||= Braintree::Subscription.find(token)
+ end
+
+ def merchant_account_time_zone
+ ActiveSupport::TimeZone[Ashmont.merchant_account_time_zone]
+ end
+ end
+end
View
45 spec/ashmont/errors_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Ashmont::Errors do
+ it "adds an error for a declined number" do
+ errors_for(:status => "processor_declined", :processor_response_text => "failure").
+ should include("number was denied by the payment processor with the message: failure")
+ end
+
+ it "adds an error for a mismatched cvv" do
+ errors_for(:status => "gateway_rejected").should include("cvv did not match")
+ end
+
+ it "adds a generic card number error" do
+ errors_for(:messages => { :number => "Credit card number is unsupported" }).
+ should include("number is unsupported")
+ end
+
+ it "adds a generic cvv error" do
+ errors_for(:messages => { :CVV => "CVV is unsupported" }).
+ should include("cvv is unsupported")
+ end
+
+ it "adds a generic expiration_month error" do
+ errors_for(:messages => { :expiration_month => "Expiration month is unsupported" }).
+ should include("expiration_month is unsupported")
+ end
+
+ it "adds a generic expiration_year error" do
+ errors_for(:messages => { :expiration_year => "Expiration year is unsupported" }).
+ should include("expiration_year is unsupported")
+ end
+
+ def errors_for(options = {})
+ result = stub("result",
+ :status => options[:status] || "rejected",
+ :processor_response_text => options[:processor_response_text] || "error")
+ errors = (options[:messages] || {}).map do |attribute, message|
+ stub("error", :attribute => attribute.to_s, :message => message)
+ end
+
+ Ashmont::Errors.new(result, errors).to_hash.inject([]) do |result, (attribute, message)|
+ result << [attribute, message].join(" ")
+ end
+ end
+end
View
116 spec/ashmont/subscription_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe Ashmont::Subscription do
+ %w(transactions status).each do |delegated_method|
+ it "delegates ##{delegated_method} to the remote subscription" do
+ token = 'xyz'
+ remote_subscription = stub("remote-subscription", delegated_method => "expected")
+ Braintree::Subscription.stubs(:find => remote_subscription)
+ subscription = Ashmont::Subscription.new(:token => token)
+ result = subscription.send(delegated_method)
+ Braintree::Subscription.should have_received(:find).with(token)
+ result.should == "expected"
+ end
+ end
+
+ it "converts the next billing date into the configured timezone" do
+ unconverted_date = "2011-01-20"
+ remote_subscription = stub("remote-subscription", :next_billing_date => unconverted_date)
+ Braintree::Subscription.stubs(:find => remote_subscription)
+ subscription = Ashmont::Subscription.new
+ result = subscription.next_billing_date
+ result.utc_offset.should == ActiveSupport::TimeZone[Ashmont.merchant_account_time_zone].utc_offset
+ result.strftime("%Y-%m-%d").should == unconverted_date
+ end
+
+ it "returns the token" do
+ subscription = Ashmont::Subscription.new(:token => 'abc')
+ subscription.token.should == 'abc'
+ end
+
+ it "retries a subscription" do
+ subscription_token = 'xyz'
+ transaction_token = 'abc'
+ transaction = stub("transaction", :id => transaction_token)
+ retry_result = stub("retry-result", :transaction => transaction)
+ settlement_result = stub("settlement-result", :success? => true)
+ Braintree::Subscription.stubs(:retry_charge => retry_result)
+ Braintree::Transaction.stubs(:submit_for_settlement => settlement_result)
+
+ subscription = Ashmont::Subscription.new(:token => subscription_token)
+ subscription.retry_charge.should be_true
+ subscription.errors.should be_empty
+
+ Braintree::Subscription.should have_received(:retry_charge).with(subscription_token)
+ Braintree::Transaction.should have_received(:submit_for_settlement).with(transaction_token)
+ end
+
+ it "has errors after a failed subscription retry" do
+ subscription_token = 'xyz'
+ transaction_token = 'abc'
+ transaction = stub("transaction", :id => transaction_token)
+ error_messages = "failure"
+ errors = "errors"
+ retry_result = stub("retry-result", :transaction => transaction)
+ settlement_result = stub("settlement-result", :success? => false, :errors => error_messages)
+ Braintree::Subscription.stubs(:retry_charge => retry_result)
+ Braintree::Transaction.stubs(:submit_for_settlement => settlement_result)
+ Ashmont::Errors.stubs(:new => errors)
+
+ subscription = Ashmont::Subscription.new(:token => subscription_token)
+ subscription.retry_charge.should be_false
+ subscription.errors.should == errors
+
+ Braintree::Subscription.should have_received(:retry_charge).with(subscription_token)
+ Braintree::Transaction.should have_received(:submit_for_settlement).with(transaction_token)
+ Ashmont::Errors.should have_received(:new).with(transaction, error_messages)
+ end
+
+ it "updates a subscription" do
+ token = 'xyz'
+ Braintree::Subscription.stubs(:update => 'expected')
+
+ subscription = Ashmont::Subscription.new(:token => token, :name => "Billy")
+ result = subscription.save
+
+ Braintree::Subscription.should have_received(:update).with(token, :name => "Billy")
+ result.should == "expected"
+ end
+
+ it "creates a successful subscription" do
+ attributes = { "test" => "hello" }
+ token = "xyz"
+ remote_subscription = stub('remote-subscription', :id => token, :status => "fine")
+ result = stub("result", :subscription => remote_subscription, :success? => true)
+ Braintree::Subscription.stubs(:create => result)
+
+ subscription = Ashmont::Subscription.new(attributes)
+ subscription.save.should be_true
+
+ Braintree::Subscription.should have_received(:create).with(attributes)
+ subscription.token.should == token
+ subscription.status.should == "fine"
+ end
+
+ it "returns the most recent transaction" do
+ Timecop.freeze(Time.now) do
+ dates = [2.days.ago, 3.days.ago, 1.day.ago]
+ transactions = dates.map { |date| stub("transaction", :created_at => date) }
+ remote_subscription = stub("remote-subscription", :transactions => transactions)
+ Braintree::Subscription.stubs(:find => remote_subscription)
+
+ subscription = Ashmont::Subscription.new
+
+ subscription.most_recent_transaction.created_at.should == 1.day.ago
+ end
+ end
+
+ it "reloads remote data" do
+ old_remote_subscription = stub("old-remote-subscription", :status => "old")
+ new_remote_subscription = stub("new-remote-subscription", :status => "new")
+ Braintree::Subscription.stubs(:find).returns(old_remote_subscription).then.returns(new_remote_subscription)
+ subscription = Ashmont::Subscription.new(:token => 'xyz')
+ subscription.status.should == "old"
+ subscription.reload.status.should == "new"
+ end
+end
View
8 spec/spec_helper.rb
@@ -0,0 +1,8 @@
+require 'rspec'
+require 'ashmont'
+require 'bourne'
+require 'timecop'
+
+RSpec.configure do |config|
+ config.mock_with :mocha
+end
Please sign in to comment.
Something went wrong with that request. Please try again.