diff --git a/lib/active_merchant/billing/gateways/beanstream.rb b/lib/active_merchant/billing/gateways/beanstream.rb
index bb66f8d75fc..17c9a210d8a 100644
--- a/lib/active_merchant/billing/gateways/beanstream.rb
+++ b/lib/active_merchant/billing/gateways/beanstream.rb
@@ -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 :user and :password options.
@@ -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
diff --git a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb
index 973d4612415..1e2293ea916 100644
--- a/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb
+++ b/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb
@@ -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'
@@ -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'
@@ -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
@@ -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]
@@ -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
@@ -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
@@ -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
diff --git a/test/fixtures.yml b/test/fixtures.yml
index b8ba3ff0877..e70cb573066 100644
--- a/test/fixtures.yml
+++ b/test/fixtures.yml
@@ -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
diff --git a/test/remote/gateways/remote_beanstream_test.rb b/test/remote/gateways/remote_beanstream_test.rb
index 39d2fe684c5..be7b3ebca33 100644
--- a/test/remote/gateways/remote_beanstream_test.rb
+++ b/test/remote/gateways/remote_beanstream_test.rb
@@ -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
@@ -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 => '',
diff --git a/test/unit/gateways/beanstream_test.rb b/test/unit/gateways/beanstream_test.rb
index 6f35a4d0f38..536feba20a8 100644
--- a/test/unit/gateways/beanstream_test.rb
+++ b/test/unit/gateways/beanstream_test.rb
@@ -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
@@ -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
@@ -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
+ "1
Request successful"
+ end
+
+ def successful_cancel_recurring_response
+ "1
Request successful"
+ end
+
end
+