Permalink
Browse files

a simple Subscription#credit_card= writer, which may not appropriate …

…for all situations.

abstracting out a Freemium::Response object. note that this is accompanied by a change in the return value from the existing BrainTree interface.
  • Loading branch information...
1 parent 3e6c781 commit 2dc46bda53afae377301a4f4b857adb8bc1ec346 @cainlevy committed Apr 16, 2008
View
@@ -1,4 +1,6 @@
module Freemium
+ class CreditCardStorageError < RuntimeError; end
+
class << self
# Lets you configure which ActionMailer class contains appropriate
# mailings for invoices, expiration warnings, and expiration notices.
@@ -1,6 +1,31 @@
module Freemium
module Gateways
class Base #:nodoc:
+ # cancels the subscription identified by the given billing key.
+ # this might mean removing it from the remote system, or halting the remote
+ # recurring billing.
+ #
+ # should return a Freemium::Response
+ def cancel(billing_key)
+ raise MethodNotImplemented
+ end
+
+ # stores a credit card with the gateway.
+ # should return a Freemium::Response
+ def store(credit_card, address = nil)
+ raise MethodNotImplemented
+ end
+
+ # updates a credit card in the gateway.
+ # should return a Freemium::Response
+ def update(billing_key, credit_card = nil, address = nil)
+ raise MethodNotImplemented
+ end
+
+ ##
+ ## Only needed to support Freemium.billing_controller = :arb
+ ##
+
# only needed to support an ARB module. otherwise, the manual billing process will
# take care of processing transaction information as it happens.
#
@@ -14,20 +39,13 @@ def transactions(options = {})
raise MethodNotImplemented
end
- # charges money against the given billing key. only used for manual billing
- # processes, and so may not be appropriate for all concrete classes.
- #
- # return value should be a single Freemium::Transaction object.
- def charge(billing_key, amount)
- raise MethodNotImplemented
- end
+ ##
+ ## Only needed to support Freemium.billing_controller = :freemium
+ ##
- # cancels the subscription identified by the given billing key.
- # this might mean removing it from the remote system, or halting the remote
- # recurring billing.
- #
- # return value is ignored.
- def cancel(billing_key)
+ # charges money against the given billing key.
+ # should return a Freemium::Transaction
+ def charge(billing_key, amount)
raise MethodNotImplemented
end
end
@@ -36,7 +36,7 @@ def store(credit_card, address = nil)
p.params.merge! params_for_credit_card(credit_card)
p.params.merge! params_for_address(address) if address
p.commit
- return p
+ return p.response
end
# Updates a card in SecureVault.
@@ -50,7 +50,7 @@ def update(vault_id, credit_card = nil, address = nil)
p.params.merge! params_for_credit_card(credit_card) if credit_card
p.params.merge! params_for_address(address) if address
p.commit
- return p
+ return p.response
end
# Manually charges a card in SecureVault. Called automatically as part of manual billing process.
@@ -62,7 +62,7 @@ def charge(vault_id, amount)
:amount => sprintf("%.2f", amount.cents.to_f / 100)
})
p.commit
- return Freemium::Transaction.new(:billing_key => vault_id, :amount => amount, :success => p.success?)
+ return Freemium::Transaction.new(:billing_key => vault_id, :amount => amount, :success => p.response.success?)
end
# Removes a card from SecureVault. Called automatically when the subscription expires.
@@ -74,7 +74,7 @@ def cancel(vault_id)
:customer_vault_id => vault_id
})
p.commit
- return p.success?
+ return p.response
end
protected
@@ -102,22 +102,22 @@ def params_for_address(address)
class Post
attr_accessor :url
- attr_accessor :response
attr_accessor :params
+ attr_reader :response
def initialize(url, params = {})
self.url = url
self.params = params
end
def commit
- self.response = parse(post)
- return self
- end
-
- def success?
+ data = parse(post)
# from BT API: 1 means approved, 2 means declined, 3 means error
- self.response['response'].to_i == 1
+ success = data['response'].to_i == 1
+ @response = Response.new(success, data)
+ @response.billing_key = data['customer_vault_id']
+ @response.message = data['responsetext']
+ return self
end
protected
View
@@ -0,0 +1,24 @@
+module Freemium
+ # used to encapsulate the success/failure/details of a response from some gateway.
+ # intended to be independent of the details of communication (e.g. Freemium::Gateways::BrainTree::Post).
+ class Response
+ # a gateway-specific hash of raw data related to the request.
+ attr_reader :raw_data
+ # may contain a description of the response. should contain an explanation if the response was not a success.
+ attr_accessor :message
+ # the related billing key, if appropriate
+ attr_accessor :billing_key
+
+ def initialize(success, raw_data = {})
+ @success, @raw_data = success, raw_data
+ end
+
+ def success?
+ @success
+ end
+
+ def [](key)
+ raw_data[key]
+ end
+ end
+end
View
@@ -95,6 +95,24 @@ def expired?
expire_on and expire_on <= Date.today
end
+ # Simple assignment of a credit card. Note that this may not be
+ # useful for your particular situation, especially if you need
+ # to simultaneously set up automated recurrences.
+ #
+ # Because of the third-party interaction with the gateway, you
+ # need to be careful to only use this method when you expect to
+ # be able to save the record successfully. Otherwise you may end
+ # up storing a credit card in the gateway and then losing the key.
+ #
+ # NOTE: Support for updating an address could easily be added
+ # with an "address" property on the credit card.
+ def credit_card=(cc)
+ response = (billing_key) ? Freemium.gateway.update(billing_key, cc) : Freemium.gateway.store(cc)
+ raise Freemium::CreditCardStorageError.new(response.message) unless response.success?
+ self.billing_key = response.billing_key
+ return cc
+ end
+
protected
# extends the paid_through period according to how much money was received.
@@ -25,11 +25,11 @@ def setup
def test_lifecycle
# store
- post = @gateway.store(@card, @address)
- vault_id = post.response['customer_vault_id']
- assert post.success?, "transaction was accepted"
+ response = @gateway.store(@card, @address)
+ vault_id = response.billing_key
+ assert response.success?, "transaction was accepted"
assert_not_nil vault_id, "customer was assigned a vault_id"
- assert_equal "Customer Added", post.response['responsetext']
+ assert_equal "Customer Added", response['responsetext']
# charge
transaction = @gateway.charge(vault_id, Money.new(1295))
@@ -39,34 +39,35 @@ def test_lifecycle
# update
@card.last_name = "Burninator"
- post = @gateway.update(vault_id, @card)
- assert post.success?
- assert_equal "Customer Update Successful", post.response['responsetext']
+ response = @gateway.update(vault_id, @card)
+ assert response.success?
+ assert_equal "Customer Update Successful", response.message
# delete (cancel)
- assert @gateway.cancel(vault_id)
+ response = @gateway.cancel(vault_id)
+ assert response.success?
end
def test_failed_storage
@card.number = ''
- post = @gateway.store(@card, @address)
- assert !post.success?
- assert post.response['response']
+ response = @gateway.store(@card, @address)
+ assert !response.success?
+ assert response['response']
end
def test_failed_charge
- post = @gateway.store(@card, @address)
- vault_id = post.response['customer_vault_id']
+ response = @gateway.store(@card, @address)
+ vault_id = response.billing_key
# any amount under one dollar should fail in the BrainTree test environment
transaction = @gateway.charge(vault_id, Money.new(54))
assert !transaction.success?
end
def test_storage_without_address
- post = @gateway.store(@card)
- assert post.success?, "transaction was accepted"
- assert_not_nil post.response['customer_vault_id']
- assert_equal "Customer Added", post.response['responsetext']
+ response = @gateway.store(@card)
+ assert response.success?, "transaction was accepted"
+ assert_not_nil response.billing_key
+ assert_equal "Customer Added", response.message
end
end
@@ -144,6 +144,42 @@ def test_deleting_cancels_in_gateway
subscriptions(:bobs_subscription).destroy
end
+ ##
+ ## The Subscription#credit_card= shortcut
+ ##
+ def test_adding_a_credit_card
+ subscription = Subscription.new
+ cc = Freemium::CreditCard.new
+ response = Freemium::Response.new(true)
+ response.billing_key = "alphabravo"
+ Freemium.gateway.expects(:store).with(cc).returns(response)
+
+ assert_nothing_raised do subscription.credit_card = cc end
+ assert_equal "alphabravo", subscription.billing_key
+ assert subscription.new_record?
+ end
+
+ def test_updating_a_credit_card
+ subscription = Subscription.find(:first, :conditions => "billing_key IS NOT NULL")
+ cc = Freemium::CreditCard.new
+ response = Freemium::Response.new(true)
+ response.billing_key = "new code"
+ Freemium.gateway.expects(:update).with(subscription.billing_key, cc).returns(response)
+
+ assert_nothing_raised do subscription.credit_card = cc end
+ assert_equal "new code", subscription.billing_key, "catches any change to the billing key"
+ assert subscription.reload.billing_key != "new code", "change was not saved"
+ end
+
+ def test_failing_to_add_a_credit_card
+ subscription = Subscription.new
+ cc = Freemium::CreditCard.new
+ response = Freemium::Response.new(false)
+ Freemium.gateway.expects(:store).returns(response)
+
+ assert_raises Freemium::CreditCardStorageError do subscription.credit_card = cc end
+ end
+
protected
def create_subscription(options = {})

0 comments on commit 2dc46bd

Please sign in to comment.