At Inner Fence, we have a large number of retail merchants who use our Credit Card Terminal app for face-to-face or over-the-phone transactions. Cardholder information is keyed in or swiped using our iPhone Credit Card Reader accessory. New personnel have to be able to use our app with very little training, and the small and medium businesses that comprise the bulk of our merchants don’t have a dedicated IT staff. In other words, it’s a little bit different than dealing with e-commerce web site administrators, and our focus is always on simplicity of user experience.
We integrate with Authorize.Net using the Advanced Integration Method. Based on the simple interface we want to present, it was a no-brainer to choose AUTH_CAPTURE to model our merchants’ idea of charging a card as a point event; in a vast majority of retail scenarios, restaurants and equipment rental excluded, there’s no need to use AUTH_ONLY and PRIOR_AUTH_CAPTURE.
There is no similar unified operation for refunding money, though. AIM only provides the separate VOID and CAPTURE operations, and using the wrong one will produce errors that retailers may find frustrating to deal with.
In Credit Card Terminal, we decided to provide a unified refund operation in the user interface, hiding the complexity of the underlying API. I’m going to go over a basic implementation, including sample code, but first let’s review exactly what VOID and CREDIT are and why our retail merchants are unlikely to understand the distinction.
Void/Credit—The Developer’s Perspective
The key difference between VOID and CREDIT relates to the idea of settlement. In non-Authorize.Net environments, settlement is often referred to as “batching out”. (We’ll talk about why Authorize.Net merchants don’t talk about it this way in the next section.)
The general idea is that throughout the day as you’re running transactions, you’re building a “batch”. Then, at the end of the day you “batch out”, submitting all the transactions for settlement at once.
VOID only applies to unsettled transactions. Essentially, to VOID a transaction removes it from the batch so that it’s never settled. However, once a transaction has settled, any attempt to VOID it will result in an error. It’s also limited in the sense that your only option is to give the customer back all the money; you can’t partially VOID a transaction.
CREDIT—as you might have guessed—only applies to settled transactions. It’s a more flexible operation, allowing you to give back all or part of the original amount. You can even perform a CREDIT several times against the same original transaction, so long as the total amount credited does not exceed the original charge. Attempting to CREDIT an unsettled transaction or a transaction older than 120 days will result in an error.
Refunds—The Merchant’s Perspective
From the merchant’s point of view, both transactions just look like a refund. Maybe there was a mistake and the wrong amount was charged, or maybe the customer is returning merchandise, but either way the general idea is that money is going back to the customer.
A merchant is especially unlikely to understand the distinction when using Authorize.Net, because—unlike many other payment platforms—Authorize.Net automatically settles transactions without any manual step on the merchant’s part. This makes Authorize.Net phenomenally more user-friendly and convenient, but it compounds the VOID/CREDIT confusion.
Authorize.Net merchants, relieved of the tedium of submitting batches manually, may not know whether a given transaction is settled. Such a merchant is unlikely to have any better approach than trial-and-error when confronted with buttons labeled “Void” and “Credit”. We propose that you should collapse them into a single “Refund” action in your app. If trial-and-error is the best option, let the computer do it; the computer is much less prone to frustration than a human merchant.
Partial Refunds—The Edge Case
The details-oriented reader will already have noticed that we have a problem if the merchant wants to refund only part of the transaction. Partial refunds require CREDIT, which only works on settled transactions.
Our “smart” refund is going to need to raise an error in this case, explaining that the merchant needs to refund the full amount, then do a new charge for the smaller amount. Admittedly, this isn’t ideal, but with Credit Card Terminal, we’ve found that a great majority of refunds are for the full amount, so we’re willing to sacrifice elegance on this one edge case to improve the overall experience for the user.
Depending on your app and environment, you may be able to make a different choice for this. Credit Card Terminal does not have access to the credit card number, expiration, or CVV, but if your app does, you might consider automating the void-then-recharge flow instead of having an error message for this case.
Unified Refund—The General Approach
Our goal is to create an intuitive Refund function in our app—one the merchant can count on to DWIM (Do What I Mean). Let’s consult the AIM Guide to come up with a basic approach that will work.
There’s plenty of room for refining this simple approach. For example, you could use a time-based heuristic to eliminate the first VOID. But for our purposes, this will do fine. Since refunds are exceptional events, the overhead of one extra network request is unlikely to impact the merchant too much.
Sample Code—Ruby & Active Merchant
For our sample, I’m going to do Ruby code that uses the popular Active Merchant gem for the Authorize.Net implementation. Since active_merchant does all the heavy lifting, it’s a great way to present this approach at a high level you should be able to follow whatever your language of choice. BTW, our Credit Card Terminal implementations are in Objective C (iPhone/iPad/Mac), Java (Android), and C# (Windows Phone).
#!/usr/bin/env ruby require 'rubygems' require 'active_merchant' # Since this is a sample, use a hard-coded sandbox setup - # you'll need to plug in your sandbox credentials. ActiveMerchant::Billing::Base.mode = :test SampleGateway = ActiveMerchant::Billing::AuthorizeNetGateway.new( :login => 'YOUR_SANDBOX_LOGIN_ID', :password => 'YOUR_SANDBOX_TRANSACTION_KEY' ) # Our unified refund operation would likely go onto some kind of # "model" or biz object that represents a purchase in your system. # Since this is a sample, we'll just mock it up as a struct with the # bare minimum required info. class Transaction < Struct.new( :transaction_id, :amount_charged, :cc_last_four ) # We'll raise this exception in the case of an unsettled credit. class UnsettledCreditError < RuntimeError UNSETTLED_CREDIT_RESPONSE_REASON_CODE = '54' def self.match?( response ) response.params['response_reason_code'] == UNSETTLED_CREDIT_RESPONSE_REASON_CODE end end def initialize( transaction_id, amount_charged, cc_last_four ) self.transaction_id = transaction_id self.amount_charged = amount_charged self.cc_last_four = cc_last_four end def refund( amount ) if amount != self.amount_charged # Different amounts: only a CREDIT will do response = SampleGateway.credit( amount, self.transaction_id, :card_number => self.cc_last_four ) if UnsettledCreditError.match?( response ) raise UnsettledCreditError end else # Same amount: try a VOID first, falling back to CREDIT if that fails response = SampleGateway.void( self.transaction_id ) if !response.success? response = SampleGateway.credit( amount, self.transaction_id, :card_number => self.cc_last_four ) end end response end end # Let's include a little bit of code to exercise our new operation; if # you actually want to be able to do the CREDIT, you'll need to run a # transaction and wait for it to settle, then modify the code to have # that transaction id. Since we're just working with a purchase that # was made moments ago, it won't have settled, and we'll always end up # failing the partial refunding and doing the full refund with a VOID. credit_card = ActiveMerchant::Billing::CreditCard.new( :number => '4111111111111111', :month => 1, :year => 2015, ) charge_amount = rand(1000) + 10 # Random amount to avoid dupe detection response = SampleGateway.purchase( charge_amount, credit_card ) puts "Initial purchase: #{response.message}" exit 1 unless response.success? transaction_biz_object = Transaction.new( response.params['transaction_id'], charge_amount, credit_card.number[-4..-1] ) partial_refund_amount = charge_amount - 9 begin response = transaction_biz_object.refund( partial_refund_amount ) puts "Partial refund: #{response.message}" rescue Transaction::UnsettledCreditError puts "Partial refund: Must do full refund then rerun for the correct amount." end response = transaction_biz_object.refund( charge_amount ) puts "Full refund: #{response.message}"
The great thing about nailing an intuitive interface for your users is that you hear very little in the way of complaints or support requests about the feature. That’s how this has been for us — pretty much blissfully uneventful. As the developer, I love it, since I can devote my time to developing cool new stuff instead of supporting the old stuff.
/rdj
---
Ryan D Johnson is the lead developer for Inner Fence, a Seattle-based software company focused on mobile payment solutions, including Credit Card Terminal for iPhone, iPad, Mac, Android, and Windows Phone. Inner Fence is an Authorize.Net Trusted Partner and guest contributor to this blog.