require 'active_merchant/billing/gateways/migs/migs_codes'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class MigsGateway < Gateway
include MigsCodes
API_VERSION = 1
class_attribute :server_hosted_url, :merchant_hosted_url
self.server_hosted_url = 'https://migs.mastercard.com.au/vpcpay'
self.merchant_hosted_url = 'https://migs.mastercard.com.au/vpcdps'
self.live_url = self.server_hosted_url
# MiGS is supported throughout Asia Pacific, Middle East and Africa
# MiGS is used in Australia (AU) by ANZ (eGate), CBA (CommWeb) and more
# Source of Country List: http://www.scribd.com/doc/17811923
self.supported_countries = %w(AU AE BD BN EG HK ID JO KW LB LK MU MV MY NZ OM PH QA SA SG TT VN)
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb]
self.money_format = :cents
self.currencies_without_fractions = %w(IDR)
# The homepage URL of the gateway
self.homepage_url = 'http://mastercard.com/mastercardsps'
# The name of the gateway
self.display_name = 'MasterCard Internet Gateway Service (MiGS)'
# Creates a new MigsGateway
# The advanced_login/advanced_password fields are needed for
# advanced methods such as the capture, refund and status methods
#
# ==== Options
#
# * :login -- The MiGS Merchant ID (REQUIRED)
# * :password -- The MiGS Access Code (REQUIRED)
# * :secure_hash -- The MiGS Secure Hash
# (Required for Server Hosted payments)
# * :advanced_login -- The MiGS AMA User
# * :advanced_password -- The MiGS AMA User's password
def initialize(options = {})
requires!(options, :login, :password)
super
end
# ==== Options
#
# * :order_id -- A reference for tracking the order (REQUIRED)
# * :unique_id -- A unique id for this request (Max 40 chars).
# If not supplied one will be generated.
def purchase(money, creditcard, options = {})
requires!(options, :order_id)
post = {}
add_amount(post, money, options)
add_invoice(post, options)
add_creditcard(post, creditcard)
add_standard_parameters('pay', post, options[:unique_id])
add_3ds(post, options)
add_tx_source(post, options)
commit(post)
end
# MiGS works by merchants being either purchase only or authorize/capture
# So authorize is the same as purchase when in authorize mode
alias_method :authorize, :purchase
# ==== Options
#
# * :unique_id -- A unique id for this request (Max 40 chars).
# If not supplied one will be generated.
def capture(money, authorization, options = {})
requires!(@options, :advanced_login, :advanced_password)
post = options.merge(:TransNo => authorization)
add_amount(post, money, options)
add_advanced_user(post)
add_standard_parameters('capture', post, options[:unique_id])
add_tx_source(post, options)
commit(post)
end
# ==== Options
#
# * :unique_id -- A unique id for this request (Max 40 chars).
# If not supplied one will be generated.
def refund(money, authorization, options = {})
requires!(@options, :advanced_login, :advanced_password)
post = options.merge(:TransNo => authorization)
add_amount(post, money, options)
add_advanced_user(post)
add_standard_parameters('refund', post, options[:unique_id])
add_tx_source(post, options)
commit(post)
end
def void(authorization, options = {})
requires!(@options, :advanced_login, :advanced_password)
post = options.merge(:TransNo => authorization)
add_advanced_user(post)
add_standard_parameters('voidAuthorisation', post, options[:unique_id])
add_tx_source(post, options)
commit(post)
end
def credit(money, authorization, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
def verify(credit_card, options={})
MultiResponse.run do |r|
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
# Checks the status of a previous transaction
# This can be useful when a response is not received due to network issues
#
# ==== Parameters
#
# * unique_id -- Unique id of transaction to find.
# This is the value of the option supplied in other methods or
# if not supplied is returned with key :MerchTxnRef
def status(unique_id)
requires!(@options, :advanced_login, :advanced_password)
post = {}
add_advanced_user(post)
add_standard_parameters('queryDR', post, unique_id)
commit(post)
end
# Generates a URL to redirect user to MiGS to process payment
# Once user is finished MiGS will redirect back to specified URL
# With a response hash which can be turned into a Response object
# with purchase_offsite_response
#
# ==== Options
#
# * :order_id -- A reference for tracking the order (REQUIRED)
# * :locale -- Change the language of the redirected page
# Values are 2 digit locale, e.g. en, es
# * :return_url -- the URL to return to once the payment is complete
# * :card_type -- Providing this skips the card type step.
# Values are ActiveMerchant formats: e.g. master, visa, american_express, diners_club
# * :unique_id -- Unique id of transaction to find.
# If not supplied one will be generated.
def purchase_offsite_url(money, options = {})
requires!(options, :order_id, :return_url)
requires!(@options, :secure_hash)
post = {}
add_amount(post, money, options)
add_invoice(post, options)
add_creditcard_type(post, options[:card_type]) if options[:card_type]
post[:Locale] = options[:locale] || 'en'
post[:ReturnURL] = options[:return_url]
add_standard_parameters('pay', post, options[:unique_id])
add_secure_hash(post)
self.server_hosted_url + '?' + post_data(post)
end
# Parses a response from purchase_offsite_url once user is redirected back
#
# ==== Parameters
#
# * data -- All params when offsite payment returns
# e.g. returns to http://company.com/return?a=1&b=2, then input "a=1&b=2"
def purchase_offsite_response(data)
requires!(@options, :secure_hash)
response_hash = parse(data)
expected_secure_hash = calculate_secure_hash(response_hash, @options[:secure_hash])
unless response_hash[:SecureHash] == expected_secure_hash
raise SecurityError, 'Secure Hash mismatch, response may be tampered with'
end
response_object(response_hash)
end
def test?
@options[:login].start_with?('TEST')
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((&?CardNum=)\d*(&?)), '\1[FILTERED]\2').
gsub(%r((&?CardSecurityCode=)\d*(&?)), '\1[FILTERED]\2').
gsub(%r((&?AccessCode=)[^&]*(&?)), '\1[FILTERED]\2').
gsub(%r((&?Password=)[^&]*(&?)), '\1[FILTERED]\2').
gsub(%r((&?3DSXID=)[^&]*(&?)), '\1[FILTERED]\2').
gsub(%r((&?VerToken=)[^&]*(&?)), '\1[FILTERED]\2')
end
private
def add_amount(post, money, options)
post[:Amount] = localized_amount(money, options[:currency])
post[:Currency] = options[:currency] if options[:currency]
end
def add_advanced_user(post)
post[:User] = @options[:advanced_login]
post[:Password] = @options[:advanced_password]
end
def add_invoice(post, options)
post[:OrderInfo] = options[:order_id]
end
def add_3ds(post, options)
post[:VerType] = options[:ver_type] if options[:ver_type]
post[:VerToken] = options[:ver_token] if options[:ver_token]
post['3DSXID'] = options[:three_ds_xid] if options[:three_ds_xid]
post['3DSECI'] = options[:three_ds_eci] if options[:three_ds_eci]
post['3DSenrolled'] = options[:three_ds_enrolled] if options[:three_ds_enrolled]
post['3DSstatus'] = options[:three_ds_status] if options[:three_ds_status]
end
def add_tx_source(post, options)
post[:TxSource] = options[:tx_source] if options[:tx_source]
end
def add_creditcard(post, creditcard)
post[:CardNum] = creditcard.number
post[:CardSecurityCode] = creditcard.verification_value if creditcard.verification_value?
post[:CardExp] = format(creditcard.year, :two_digits) + format(creditcard.month, :two_digits)
end
def add_creditcard_type(post, card_type)
post[:Gateway] = 'ssl'
post[:card] = CARD_TYPES.detect { |ct| ct.am_code == card_type }.migs_long_code
end
def parse(body)
params = CGI::parse(body)
hash = {}
params.each do |key, value|
hash[key.gsub('vpc_', '').to_sym] = value[0]
end
hash
end
def commit(post)
add_secure_hash(post) if @options[:secure_hash]
data = ssl_post self.merchant_hosted_url, post_data(post)
response_hash = parse(data)
response_object(response_hash)
end
def response_object(response)
avs_response_code = response[:AVSResultCode]
avs_response_code = 'S' if avs_response_code == 'Unsupported'
cvv_result_code = response[:CSCResultCode]
cvv_result_code = 'P' if cvv_result_code == 'Unsupported'
Response.new(success?(response), response[:Message], response,
:test => test?,
:authorization => response[:TransactionNo],
:fraud_review => fraud_review?(response),
:avs_result => { :code => avs_response_code },
:cvv_result => cvv_result_code
)
end
def success?(response)
response[:TxnResponseCode] == '0'
end
def fraud_review?(response)
ISSUER_RESPONSE_CODES[response[:AcqResponseCode]] == 'Suspected Fraud'
end
def add_standard_parameters(action, post, unique_id = nil)
post.merge!(
:Version => API_VERSION,
:Merchant => @options[:login],
:AccessCode => @options[:password],
:Command => action,
:MerchTxnRef => unique_id || generate_unique_id.slice(0, 40)
)
end
def post_data(post)
post.collect { |key, value| "vpc_#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
def add_secure_hash(post)
post[:SecureHash] = calculate_secure_hash(post, @options[:secure_hash])
post[:SecureHashType] = 'SHA256'
end
def calculate_secure_hash(post, secure_hash)
input = post.
reject { |k| %i[SecureHash SecureHashType].include?(k) }.
sort.
map { |(k, v)| "vpc_#{k}=#{v}" }.
join('&')
OpenSSL::HMAC.hexdigest('SHA256', [secure_hash].pack('H*'), input).upcase
end
end
end
end