require 'active_merchant/billing/gateways/migs/migs_codes'
require 'digest/md5' # Used in add_secure_hash
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 IN 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)
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])
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])
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])
commit(post)
end
def credit(money, authorization, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
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.merge!(
:Locale => options[:locale] || 'en',
: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.reject{|k, v| k == :SecureHash}, @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
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_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)
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])
end
def calculate_secure_hash(post, secure_hash)
sorted_values = post.sort_by(&:to_s).map(&:last)
input = secure_hash + sorted_values.join
Digest::MD5.hexdigest(input).upcase
end
end
end
end