Skip to content

Commit

Permalink
Implement Beanstream recurring paymentments
Browse files Browse the repository at this point in the history
Original code: neerajkumar/active_merchant

Changes made:
* Remove profile reporting
* Remove old and seemingly defunct profile reporting code
* Remove unused recurring_response_notification
* Refactor recurring API to match other ActiveMerchant implementations
  * #new takes :recurring_api_key for recurring authentication
  * #recurring takes :periodicity option, as well as :interval as
    either a Hash or as an ActiveSupport::Duration (1.month).
  * #recurring takes :occurences or :payments option and is used to
    calculate an expiry date

Special thanks to `git diff --patience -U1` and `patch -l -F2`
  • Loading branch information
Dave Lee committed Feb 27, 2012
1 parent 7c89bfa commit 15fae40
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 5 deletions.
30 changes: 29 additions & 1 deletion lib/active_merchant/billing/gateways/beanstream.rb
Expand Up @@ -19,7 +19,6 @@ module Billing #:nodoc:
# To learn more about storing credit cards with the Beanstream gateway, please read the BEAN_Payment_Profiles.pdf (I had to phone BeanStream to request it.)
#
# == Notes
# * Recurring billing is not yet implemented.
# * Adding of order products information is not implemented.
# * Ensure that country and province data is provided as a code such as "CA", "US", "QC".
# * login is the Beanstream merchant ID, username and password should be enabled in your Beanstream account and passed in using the <tt>:user</tt> and <tt>:password</tt> options.
Expand Down Expand Up @@ -95,6 +94,35 @@ def void(authorization, options = {})
commit(post)
end

def recurring(money, source, options = {})
post = {}
add_amount(post, money)
add_invoice(post, options)
add_credit_card(post, source)
add_address(post, options)
add_transaction_type(post, purchase_action(source))
add_recurring_type(post, options)
commit(post)
end

def update_recurring(amount, source, options = {})
post = {}
add_recurring_amount(post, amount)
add_recurring_invoice(post, options)
add_credit_card(post, source)
add_address(post, options)
add_recurring_operation_type(post, :update)
add_recurring_service(post, options)
recurring_commit(post)
end

def cancel_recurring(options = {})
post = {}
add_recurring_operation_type(post, :cancel)
add_recurring_service(post, options)
recurring_commit(post)
end

def interac
@interac ||= BeanstreamInteracGateway.new(@options)
end
Expand Down
108 changes: 105 additions & 3 deletions lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb
Expand Up @@ -2,6 +2,7 @@ module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module BeanstreamCore
URL = 'https://www.beanstream.com/scripts/process_transaction.asp'
RECURRING_URL = 'https://www.beanstream.com/scripts/recurring_billing.asp'
SECURE_PROFILE_URL = 'https://www.beanstream.com/scripts/payment_profile.asp'
SP_SERVICE_VERSION = '1.1'

Expand Down Expand Up @@ -37,6 +38,27 @@ module BeanstreamCore
'9' => 'I'
}

PERIODS = {
:days => 'D',
:weeks => 'W',
:months => 'M',
:years => 'Y'
}

PERIODICITIES = {
:daily => [:days, 1],
:weekly => [:weeks, 1],
:biweekly => [:weeks, 2],
:monthly => [:months, 1],
:bimonthly => [:months, 2],
:yearly => [:years, 1]
}

RECURRING_OPERATION = {
:update => 'M',
:cancel => 'C'
}

def self.included(base)
base.default_currency = 'CAD'

Expand Down Expand Up @@ -203,11 +225,66 @@ def add_secure_profile_variables(post, options = {})
post[:status] = options[:status]
end

def add_recurring_amount(post, money)
post[:amount] = amount(money)
end

def add_recurring_invoice(post, options)
post[:rbApplyTax1] = options[:apply_tax1]
end

def add_recurring_operation_type(post, operation)
post[:operationType] = RECURRING_OPERATION[operation]
end

def add_recurring_service(post, options)
post[:serviceVersion] = '1.0'
post[:merchantId] = @options[:login]
post[:passCode] = @options[:recurring_api_key]
post[:rbAccountId] = options[:account_id]
end

def add_recurring_type(post, options)
# XXX requires!
post[:trnRecurring] = 1
period, increment = interval(options)
post[:rbBillingPeriod] = PERIODS[period]
post[:rbBillingIncrement] = increment

if options.include? :start_date
post[:rbCharge] = 0
post[:rbFirstBilling] = options[:start_date].strftime('%m%d%Y')
end

if count = options[:occurrences] || options[:payments]
post[:rbExpiry] = (options[:start_date] || Date.current).advance(period => count).strftime('%m%d%Y')
end
end

def interval(options)
if options.include? :periodicity
requires!(options, [:periodicity, *PERIODICITIES.keys])
PERIODICITIES[options[:periodicity]]
elsif options.include? :interval
interval = options[:interval]
if interval.respond_to? :parts
parts = interval.parts
raise ArgumentError.new("Cannot recur with mixed interval (#{interval}). Use only one of: days, weeks, months or years") if parts.length > 1
parts.first
elsif interval.kind_of? Hash
requires!(interval, :unit)
unit, length = interval.values_at(:unit, :length)
length ||= 1
[unit, length]
end
end
end

def parse(body)
results = {}
if !body.nil?
body.split(/&/).each do |pair|
key,val = pair.split(/=/)
key, val = pair.split(/=/)
results[key.to_sym] = val.nil? ? nil : CGI.unescape(val)
end
end
Expand All @@ -221,11 +298,22 @@ def parse(body)

results
end


def recurring_parse(data)
REXML::Document.new(data).root.elements.to_a.inject({}) do |response, element|
response[element.name.to_sym] = element.text
response
end
end

def commit(params, use_profile_api = false)
post(post_data(params,use_profile_api),use_profile_api)
end

def recurring_commit(params)
recurring_post(post_data(params, false))
end

def post(data, use_profile_api=nil)
response = parse(ssl_post((use_profile_api ? SECURE_PROFILE_URL : URL), data))
response[:customer_vault_id] = response[:customerCode] if response[:customerCode]
Expand All @@ -236,7 +324,12 @@ def post(data, use_profile_api=nil)
:avs_result => { :code => (AVS_CODES.include? response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] }
)
end


def recurring_post(data)
response = recurring_parse(ssl_post(RECURRING_URL, data))
build_response(recurring_success?(response), recurring_message_from(response), response)
end

def authorization_from(response)
"#{response[:trnId]};#{response[:trnAmount]};#{response[:trnType]}"
end
Expand All @@ -245,10 +338,18 @@ def message_from(response)
response[:messageText] || response[:responseMessage]
end

def recurring_message_from(response)
response[:message]
end

def success?(response)
response[:responseType] == 'R' || response[:trnApproved] == '1' || response[:responseCode] == '1'
end

def recurring_success?(response)
response[:code] == '1'
end

def add_source(post, source)
if source.is_a?(String) or source.is_a?(Integer)
post[:customerCode] = source
Expand Down Expand Up @@ -276,6 +377,7 @@ def post_data(params, use_profile_api)

params.reject{|k, v| v.blank?}.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
end

end
end
end
Expand Down
1 change: 1 addition & 0 deletions test/fixtures.yml
Expand Up @@ -24,6 +24,7 @@ beanstream:
user: username
password: password
secure_profile_api_key: API Access Passcode
recurring_api_key: API Access Passcode

beanstream_interac:
login: merchant id
Expand Down
31 changes: 31 additions & 0 deletions test/remote/gateways/remote_beanstream_test.rb
Expand Up @@ -47,6 +47,10 @@ def setup
:tax2 => 100,
:custom => 'reference one'
}

@recurring_options = @options.merge(
:interval => { :unit => :months, :length => 1 },
:occurences => 5)
end

def test_successful_visa_purchase
Expand Down Expand Up @@ -140,6 +144,33 @@ def test_successful_check_purchase_and_credit
assert_success credit
end

def test_successful_recurring
assert response = @gateway.recurring(@amount, @visa, @recurring_options)
assert_success response
assert response.test?
assert_false response.authorization.blank?
end

def test_successful_update_recurring
assert response = @gateway.recurring(@amount, @visa, @recurring_options)
assert_success response
assert response.test?
assert_false response.authorization.blank?

assert response = @gateway.update_recurring(@amount + 500, @visa, @recurring_options.merge(:account_id => response.params["rbAccountId"]))
assert_success response
end

def test_successful_cancel_recurring
assert response = @gateway.recurring(@amount, @visa, @recurring_options)
assert_success response
assert response.test?
assert_false response.authorization.blank?

assert response = @gateway.cancel_recurring(:account_id => response.params["rbAccountId"])
assert_success response
end

def test_invalid_login
gateway = BeanstreamGateway.new(
:merchant_id => '',
Expand Down
55 changes: 54 additions & 1 deletion test/unit/gateways/beanstream_test.rb
Expand Up @@ -38,6 +38,10 @@ def setup
:tax2 => 100,
:custom => 'reference one'
}

@recurring_options = @options.merge(
:interval => { :unit => :months, :length => 1 },
:occurrences => 5)
end

def test_successful_purchase
Expand Down Expand Up @@ -115,7 +119,42 @@ def test_brazilian_address_sets_state_and_zip_to_the_required_dummy_values
@gateway.purchase(@amount, @credit_card, @options)
end


def test_successful_recurring
@gateway.expects(:ssl_post).returns(successful_recurring_response)

assert response = @gateway.recurring(@amount, @credit_card, @recurring_options)
assert_success response
assert_equal 'Approved', response.message
end

def test_successful_update_recurring
@gateway.expects(:ssl_post).returns(successful_recurring_response)

assert response = @gateway.recurring(@amount, @credit_card, @recurring_options)
assert_success response
assert_equal 'Approved', response.message

@gateway.expects(:ssl_post).returns(successful_update_recurring_response)

assert response = @gateway.update_recurring(@amount, @credit_card, @recurring_options.merge(:account_id => response.params["rbAccountId"]))
assert_success response
assert_equal "Request successful", response.message
end

def test_successful_cancel_recurring
@gateway.expects(:ssl_post).returns(successful_recurring_response)

assert response = @gateway.recurring(@amount, @credit_card, @recurring_options)
assert_success response
assert_equal 'Approved', response.message

@gateway.expects(:ssl_post).returns(successful_cancel_recurring_response)

assert response = @gateway.cancel_recurring(:account_id => response.params["rbAccountId"])
assert_success response
assert_equal "Request successful", response.message
end

private

def successful_purchase_response
Expand Down Expand Up @@ -145,4 +184,18 @@ def german_address_params_without_state
def next_year
(Time.now.year + 1).to_s[/\d\d$/]
end

def successful_recurring_response
"trnApproved=1&trnId=10000072&messageId=1&messageText=Approved&trnOrderNumber=5d9f511363a0f35d37de53b4d74f5b&authCode=&errorType=N&errorFields=&responseType=T&trnAmount=15%2E00&trnDate=6%2F4%2F2008+6%3A33%3A55+PM&avsProcessed=0&avsId=0&avsResult=0&avsAddrMatch=0&avsPostalMatch=0&avsMessage=Address+Verification+not+performed+for+this+transaction%2E&trnType=D&paymentMethod=EFT&ref1=reference+one&ref2=&ref3=&ref4=&ref5="
end

def successful_update_recurring_response
"<response><code>1</code><message>Request successful</message></response>"
end

def successful_cancel_recurring_response
"<response><code>1</code><message>Request successful</message></response>"
end

end

0 comments on commit 15fae40

Please sign in to comment.