lib/offsite_payments/integrations/realex_offsite.rb
module OffsitePayments #:nodoc:
module Integrations #:nodoc:
module RealexOffsite
mattr_accessor :production_url
mattr_accessor :test_url
self.production_url = 'https://epage.payandshop.com/epage.cgi'
self.test_url = 'https://hpp.sandbox.realexpayments.com/pay'
def self.helper(order, account, options={})
Helper.new(order, account, options)
end
def self.notification(query_string, options={})
Notification.new(query_string, options)
end
def self.return(query_string, options={})
Return.new(query_string, options)
end
def self.service_url
mode = OffsitePayments.mode
case mode
when :production
self.production_url
when :test
self.test_url
else
raise StandardError, "Integration mode set to an invalid value: #{mode}"
end
end
module Common
CURRENCY_SPECIAL_MINOR_UNITS = {
'BIF' => 0,
'BYR' => 0,
'CLF' => 0,
'CLP' => 0,
'CVE' => 0,
'DJF' => 0,
'GNF' => 0,
'HUF' => 0,
'ISK' => 0,
'JPY' => 0,
'KMF' => 0,
'KRW' => 0,
'PYG' => 0,
'RWF' => 0,
'UGX' => 0,
'UYI' => 0,
'VND' => 0,
'VUV' => 0,
'XAF' => 0,
'XOF' => 0,
'XPF' => 0,
'BHD' => 3,
'IQD' => 3,
'JOD' => 3,
'KWD' => 3,
'LYD' => 3,
'OMR' => 3,
'TND' => 3,
'COU' => 4
}
CANADIAN_STATES = {
'AB' => 'Alberta',
'BC' => 'British Columbia',
'MB' => 'Manitoba',
'NB' => 'New Brunswick',
'NL' => 'Newfoundland',
'NS' => 'Nova Scotia',
'NU' => 'Nunavut',
'NT' => 'Northwest Territories',
'ON' => 'Ontario',
'PE' => 'Prince Edward Island',
'QC' => 'Quebec',
'SK' => 'Saskatchewan',
'YT' => 'Yukon'
}
US_STATES = {
'AL' => 'Alabama',
'AK' => 'Alaska',
'AS' => 'American Samoa',
'AZ' => 'Arizona',
'AR' => 'Arkansas',
'CA' => 'California',
'CO' => 'Colorado',
'CT' => 'Connecticut',
'DE' => 'Delaware',
'DC' => 'District Of Columbia',
'FM' => 'Federated States Of Micronesia',
'FL' => 'Florida',
'GA' => 'Georgia',
'GU' => 'Guam',
'HI' => 'Hawaii',
'ID' => 'Idaho',
'IL' => 'Illinois',
'IN' => 'Indiana',
'IA' => 'Iowa',
'KS' => 'Kansas',
'KY' => 'Kentucky',
'LA' => 'Louisiana',
'ME' => 'Maine',
'MH' => 'Marshall Islands',
'MD' => 'Maryland',
'MA' => 'Massachusetts',
'MI' => 'Michigan',
'MN' => 'Minnesota',
'MS' => 'Mississippi',
'MO' => 'Missouri',
'MT' => 'Montana',
'NE' => 'Nebraska',
'NV' => 'Nevada',
'NH' => 'New Hampshire',
'NJ' => 'New Jersey',
'NM' => 'New Mexico',
'NY' => 'New York',
'NC' => 'North Carolina',
'ND' => 'North Dakota',
'MP' => 'Northern Mariana Islands',
'OH' => 'Ohio',
'OK' => 'Oklahoma',
'OR' => 'Oregon',
'PW' => 'Palau',
'PA' => 'Pennsylvania',
'PR' => 'Puerto Rico',
'RI' => 'Rhode Island',
'SC' => 'South Carolina',
'SD' => 'South Dakota',
'TN' => 'Tennessee',
'TX' => 'Texas',
'UT' => 'Utah',
'VT' => 'Vermont',
'VI' => 'Virgin Islands',
'VA' => 'Virginia',
'WA' => 'Washington',
'WV' => 'West Virginia',
'WI' => 'Wisconsin',
'WY' => 'Wyoming'
}
COUNTRY_PHONE_NUMBERS = {
'AD' => { :code => '376', :length => [6, 7, 8, 9] },
'AE' => { :code => '971', :length => [7, 8, 9] },
'AF' => { :code => '93', :length => [8, 9] },
'AG' => { :code => '1', :length => [10] },
'AI' => { :code => '1', :length => [10] },
'AL' => { :code => '355', :length => [7, 8, 9] },
'AM' => { :code => '374', :length => [8] },
'AO' => { :code => '244', :length => [9] },
'AQ' => { :code => '672', :length => [] },
'AR' => { :code => '54', :length => [8, 9] },
'AS' => { :code => '1', :length => [10] },
'AT' => { :code => '43', :length => [7, 8, 9, 10, 11, 12, 13] },
'AU' => { :code => '61', :length => [9] },
'AW' => { :code => '297', :length => [7] },
'AX' => { :code => '358', :length => [] },
'AZ' => { :code => '994', :length => [8, 9] },
'BA' => { :code => '387', :length => [8] },
'BB' => { :code => '1', :length => [10] },
'BD' => { :code => '880', :length => [10] },
'BE' => { :code => '32', :length => [8, 9] },
'BF' => { :code => '226', :length => [8] },
'BG' => { :code => '359', :length => [8, 9, 10] },
'BH' => { :code => '973', :length => [8] },
'BI' => { :code => '257', :length => [8] },
'BJ' => { :code => '229', :length => [8] },
'BL' => { :code => '590', :length => [] },
'BM' => { :code => '1', :length => [10] },
'BN' => { :code => '673', :length => [7] },
'BO' => { :code => '591', :length => [8] },
'BQ' => { :code => '599', :length => [7] },
'BR' => { :code => '55', :length => [10, 11] },
'BS' => { :code => '1', :length => [10] },
'BT' => { :code => '975', :length => [7, 8] },
'BV' => { :code => '47', :length => [] },
'BW' => { :code => '267', :length => [7] },
'BY' => { :code => '375', :length => [9] },
'BZ' => { :code => '501', :length => [7] },
'CA' => { :code => '1', :length => [10] },
'CC' => { :code => '61', :length => [9] },
'CD' => { :code => '243', :length => [8] },
'CF' => { :code => '236', :length => [8] },
'CG' => { :code => '242', :length => [7] },
'CH' => { :code => '41', :length => [9, 10] },
'CI' => { :code => '225', :length => [8] },
'CK' => { :code => '682', :length => [5] },
'CL' => { :code => '56', :length => [8, 9] },
'CM' => { :code => '237', :length => [8] },
'CN' => { :code => '86', :length => [7, 8, 9, 10, 11] },
'CO' => { :code => '57', :length => [9, 10] },
'CR' => { :code => '506', :length => [8] },
'CU' => { :code => '53', :length => [8] },
'CV' => { :code => '238', :length => [7] },
'CW' => { :code => '599', :length => [7] },
'CX' => { :code => '61', :length => [] },
'CY' => { :code => '357', :length => [8] },
'CZ' => { :code => '420', :length => [9] },
'DE' => { :code => '49', :length => [6, 7, 8, 9, 10, 11] },
'DJ' => { :code => '253', :length => [6] },
'DK' => { :code => '45', :length => [8] },
'DM' => { :code => '1', :length => [10] },
'DO' => { :code => '1', :length => [10] },
'DZ' => { :code => '213', :length => [8] },
'EC' => { :code => '593', :length => [8, 9] },
'EE' => { :code => '372', :length => [7, 8] },
'EG' => { :code => '20', :length => [9] },
'EH' => { :code => '212', :length => [] },
'ER' => { :code => '291', :length => [7] },
'ES' => { :code => '34', :length => [9] },
'ET' => { :code => '251', :length => [9] },
'FI' => { :code => '358', :length => [9] },
'FJ' => { :code => '679', :length => [7] },
'FK' => { :code => '500', :length => [5] },
'FM' => { :code => '691', :length => [7] },
'FO' => { :code => '298', :length => [6] },
'FR' => { :code => '33', :length => [9, 10] },
'GA' => { :code => '241', :length => [6, 7, 8] },
'GB' => { :code => '44', :length => [10, 11] },
'GD' => { :code => '1', :length => [10] },
'GE' => { :code => '995', :length => [9] },
'GF' => { :code => '594', :length => [10] },
'GG' => { :code => '44', :length => [] },
'GH' => { :code => '233', :length => [5, 6, 7, 8] },
'GI' => { :code => '350', :length => [8] },
'GL' => { :code => '299', :length => [6] },
'GM' => { :code => '220', :length => [7] },
'GN' => { :code => '224', :length => [8] },
'GP' => { :code => '590', :length => [10] },
'GQ' => { :code => '240', :length => [6] },
'GR' => { :code => '30', :length => [10] },
'GS' => { :code => '500', :length => [] },
'GT' => { :code => '502', :length => [8] },
'GU' => { :code => '1', :length => [10] },
'GW' => { :code => '245', :length => [7] },
'GY' => { :code => '592', :length => [6, 7] },
'HK' => { :code => '852', :length => [8] },
'HN' => { :code => '504', :length => [7, 8] },
'HR' => { :code => '385', :length => [8] },
'HT' => { :code => '509', :length => [8] },
'HU' => { :code => '36', :length => [8, 9] },
'ID' => { :code => '62', :length => [8, 9, 10, 11] },
'IE' => { :code => '353', :length => [9] },
'IL' => { :code => '972', :length => [7, 8, 9] },
'IM' => { :code => '44', :length => [] },
'IN' => { :code => '91', :length => [10] },
'IO' => { :code => '246', :length => [] },
'IQ' => { :code => '964', :length => [8, 9, 10] },
'IR' => { :code => '98', :length => [10] },
'IS' => { :code => '354', :length => [7, 8, 9] },
'IT' => { :code => '39', :length => [9, 11] },
'JE' => { :code => '44', :length => [] },
'JM' => { :code => '1', :length => [10] },
'JO' => { :code => '962', :length => [8, 9] },
'JP' => { :code => '81', :length => [9, 10] },
'KE' => { :code => '254', :length => [9] },
'KG' => { :code => '996', :length => [9] },
'KH' => { :code => '855', :length => [8] },
'KI' => { :code => '686', :length => [5] },
'KM' => { :code => '269', :length => [7] },
'KN' => { :code => '1', :length => [10] },
'KP' => { :code => '850', :length => [8, 9] },
'KR' => { :code => '82', :length => [8, 9] },
'KW' => { :code => '965', :length => [7] },
'KY' => { :code => '1', :length => [10] },
'KZ' => { :code => '7', :length => [10] },
'LA' => { :code => '856', :length => [8] },
'LB' => { :code => '961', :length => [8] },
'LC' => { :code => '1', :length => [10] },
'LI' => { :code => '423', :length => [7] },
'LK' => { :code => '94', :length => [10] },
'LR' => { :code => '231', :length => [6, 7, 8] },
'LS' => { :code => '266', :length => [8] },
'LT' => { :code => '370', :length => [8] },
'LU' => { :code => '352', :length => [9] },
'LV' => { :code => '371', :length => [8] },
'LY' => { :code => '218', :length => [8, 9] },
'MA' => { :code => '212', :length => [8] },
'MC' => { :code => '377', :length => [8, 9] },
'MD' => { :code => '373', :length => [8] },
'ME' => { :code => '382', :length => [8] },
'MF' => { :code => '590', :length => [] },
'MG' => { :code => '261', :length => [9] },
'MH' => { :code => '692', :length => [7] },
'MK' => { :code => '389', :length => [7, 8] },
'ML' => { :code => '223', :length => [8] },
'MM' => { :code => '95', :length => [7, 8] },
'MN' => { :code => '976', :length => [7, 8, 9, 10] },
'MO' => { :code => '853', :length => [8] },
'MP' => { :code => '1', :length => [10] },
'MQ' => { :code => '596', :length => [10] },
'MR' => { :code => '222', :length => [7] },
'MS' => { :code => '1', :length => [10] },
'MT' => { :code => '356', :length => [8] },
'MU' => { :code => '230', :length => [7] },
'MV' => { :code => '960', :length => [7] },
'MW' => { :code => '265', :length => [8] },
'MX' => { :code => '52', :length => [8, 9, 10] },
'MY' => { :code => '60', :length => [9, 10] },
'MZ' => { :code => '258', :length => [8, 9] },
'NA' => { :code => '264', :length => [6, 7] },
'NC' => { :code => '687', :length => [6] },
'NE' => { :code => '227', :length => [8] },
'NF' => { :code => '672', :length => [6] },
'NG' => { :code => '234', :length => [7, 8] },
'NI' => { :code => '505', :length => [8] },
'NL' => { :code => '31', :length => [9] },
'NO' => { :code => '47', :length => [8] },
'NP' => { :code => '977', :length => [7, 8] },
'NR' => { :code => '674', :length => [7] },
'NU' => { :code => '683', :length => [4] },
'NZ' => { :code => '64', :length => [8, 9] },
'OM' => { :code => '968', :length => [8] },
'PA' => { :code => '507', :length => [7] },
'PE' => { :code => '51', :length => [8, 9] },
'PF' => { :code => '689', :length => [6] },
'PG' => { :code => '675', :length => [7] },
'PH' => { :code => '63', :length => [8, 9, 10] },
'PK' => { :code => '92', :length => [9, 10] },
'PL' => { :code => '48', :length => [9] },
'PM' => { :code => '508', :length => [6] },
'PN' => { :code => '64', :length => [9] },
'PR' => { :code => '1', :length => [10] },
'PS' => { :code => '970', :length => [8] },
'PT' => { :code => '351', :length => [9] },
'PW' => { :code => '680', :length => [7] },
'PY' => { :code => '595', :length => [9] },
'QA' => { :code => '974', :length => [7] },
'RE' => { :code => '262', :length => [10] },
'RO' => { :code => '40', :length => [9] },
'RS' => { :code => '381', :length => [9] },
'RU' => { :code => '7', :length => [10] },
'RW' => { :code => '250', :length => [8, 9] },
'SA' => { :code => '966', :length => [8, 9] },
'SB' => { :code => '677', :length => [5] },
'SC' => { :code => '248', :length => [6] },
'SD' => { :code => '249', :length => [9] },
'SE' => { :code => '46', :length => [9] },
'SG' => { :code => '65', :length => [8, 9] },
'SH' => { :code => '290', :length => [4] },
'SI' => { :code => '386', :length => [8] },
'SJ' => { :code => '47', :length => [8] },
'SK' => { :code => '421', :length => [9] },
'SL' => { :code => '232', :length => [8] },
'SM' => { :code => '378', :length => [9, 10, 11, 12] },
'SN' => { :code => '221', :length => [7] },
'SO' => { :code => '252', :length => [7, 8] },
'SR' => { :code => '597', :length => [6] },
'SS' => { :code => '211', :length => [9] },
'ST' => { :code => '239', :length => [6, 7] },
'SV' => { :code => '503', :length => [8] },
'SX' => { :code => '1', :length => [10] },
'SY' => { :code => '963', :length => [7, 8] },
'SZ' => { :code => '268', :length => [7] },
'TC' => { :code => '1', :length => [10] },
'TD' => { :code => '235', :length => [7] },
'TF' => { :code => '262', :length => [] },
'TG' => { :code => '228', :length => [7] },
'TH' => { :code => '66', :length => [9, 10] },
'TJ' => { :code => '992', :length => [9] },
'TK' => { :code => '690', :length => [4] },
'TL' => { :code => '670', :length => [7] },
'TM' => { :code => '993', :length => [8] },
'TN' => { :code => '216', :length => [8] },
'TO' => { :code => '676', :length => [5, 6, 7] },
'TR' => { :code => '90', :length => [10] },
'TT' => { :code => '1', :length => [10] },
'TV' => { :code => '688', :length => [5] },
'TW' => { :code => '886', :length => [7, 8] },
'TZ' => { :code => '255', :length => [9] },
'UA' => { :code => '380', :length => [8, 9] },
'UG' => { :code => '256', :length => [9] },
'UM' => { :code => '1', :length => [] },
'US' => { :code => '1', :length => [10] },
'UY' => { :code => '598', :length => [7, 8] },
'UZ' => { :code => '998', :length => [9] },
'VA' => { :code => '39', :length => [9] },
'VC' => { :code => '1', :length => [10] },
'VE' => { :code => '58', :length => [10] },
'VG' => { :code => '1', :length => [10] },
'VI' => { :code => '1', :length => [10] },
'VN' => { :code => '84', :length => [7, 8, 9, 10] },
'VU' => { :code => '678', :length => [5, 6, 7] },
'WF' => { :code => '681', :length => [6] },
'WS' => { :code => '685', :length => [6, 7] },
'YE' => { :code => '967', :length => [7, 8, 9] },
'YT' => { :code => '262', :length => [7] },
'ZA' => { :code => '27', :length => [9] },
'ZM' => { :code => '260', :length => [9] },
'ZW' => { :code => '263', :length => [8, 9, 10, 11] }
}
def create_signature(fields, secret)
data = fields.join('.')
digest = Digest::SHA1.hexdigest(data)
signed = "#{digest}.#{secret}"
Digest::SHA1.hexdigest(signed)
end
# Realex accepts currency amounts as an integer in the lowest value
# e.g.
# format_amount(110.56, 'GBP')
# => 11056
def format_amount(amount, currency)
units = CURRENCY_SPECIAL_MINOR_UNITS[currency] || 2
multiple = 10**units
return ((amount || 0).to_d * multiple.to_d).to_i
end
# Realex returns currency amount as an integer
def format_amount_as_float(amount, currency)
units = CURRENCY_SPECIAL_MINOR_UNITS[currency] || 2
divisor = 10**units
return ((amount || 0).to_d / divisor.to_d)
end
def extract_digits(value)
return unless value
value.scan(/\d+/).join('')
end
# This method is used for generating the "BILLING_CODE" field,
# which is only needed for US, CA and GB.
# This field is generated by concatenating the zip field and
# the first line of address with a pipe(|) between them
# if the country is GB, we remove the non-numeric characters
def extract_avs_code(params={})
country_code = lookup_country_code(params[:country], :alpha2)
return unless params[:zip] && params[:address1] && ['US', 'CA', 'GB'].include?(country_code)
code = [params[:zip], params[:address1]]
code = code.collect{|p| extract_digits(p) } if params[:country] == 'GB'
code = code.reject{|p| p.empty? }.join('|')
# Since this field accepts only a few characters, we remove the ones that are not accepted,
# and trim the white spaces
code = code.gsub(/[^a-z0-9_\-| ]+/i, '').strip
end
def extract_address_match_indicator(value)
value ? 'TRUE' : 'FALSE'
end
def adjust_phone_number_length(country_calling_code, phone_number)
country_calling_code[0...3] + '|' + phone_number[0...15]
end
# The home phone number provided by the Cardholder. Should be In format:
# of 'CountryCallingCode|Number' for example, '1|123456789'.
def format_phone_number(phone_number, country_code)
return nil if phone_number.nil?
country_number = COUNTRY_PHONE_NUMBERS[country_code] || { :code => '0', :length => [] }
# Remove non-digit characters
processed_number = phone_number.gsub(/\D/, '')
return '0|0' if [[], ['0']].include? processed_number.chars.uniq
# Allow Italy and Ivory Coast to have leading zero, as they use it as a part of some phone numbers
if ['IT', 'CI'].include?(country_code) && /\A0[1-9]\d*/.match(processed_number)
return adjust_phone_number_length(country_number[:code], processed_number)
end
return '0|0' if processed_number == country_number[:code]
# Remove leading zero(s)
processed_number = processed_number.gsub(/\A0*/, '')
# Check if the potential Singapore calling code is not the local prefix
if country_code == 'SG' &&
processed_number.start_with?(country_number[:code]) &&
country_number[:length].include?(processed_number.length)
then
return adjust_phone_number_length(country_number[:code], processed_number)
end
# Remove country calling code from the processed number and try to fix trivial mistakes
if processed_number.start_with?(country_number[:code]) ||
(!(country_number[:length].include?(processed_number.length)) &&
country_number[:length].include?(processed_number.length - country_number[:code].length) &&
(country_number[:code].chars.sort == processed_number[0...country_number[:code].length].chars.sort))
then
processed_number = processed_number[country_number[:code].length..-1]
end
# Limit returned string to 3 characters + | + 15 characters
adjust_phone_number_length(country_number[:code], processed_number)
end
def lookup_state_code(country_code, state)
case country_code
when 'CA'
state_code = CANADIAN_STATES.find { |code, state_name| state_name.downcase == state.downcase}
state_code ? state_code.first : state
when 'US'
state_code = US_STATES.find { |code, state_name| state_name.downcase == state.downcase}
state_code ? state_code.first : state
end
end
# if HPP_ADDRESS_MATCH_INDICATOR is set to TRUE
# HPP requires the shipping address to be sent from the billing address
def copy_billing_address
@fields.select { |k, _| k.start_with? 'HPP_BILLING_' }
.each do |k, v|
add_field("HPP_SHIPPING_#{k.split('HPP_BILLING_')[1]}", v)
end
end
# Validations
def get_pattern(key)
case key
when 'HPP_CUSTOMER_EMAIL' then /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,24})*$/
when 'HPP_CUSTOMER_PHONENUMBER_MOBILE' then /^([0-9 +]){1,3}(\|){0,1}([0-9 +]){1,15}$/
when 'HPP_BILLING_STREET1', 'HPP_SHIPPING_STREET1', 'HPP_BILLING_STREET2', 'HPP_SHIPPING_STREET2' then /^[ÀÁÂÃÂÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåªæçèéêëìíîïðñòóôõöº÷ø¤ùúûüýþÿ~L~N~Z~\~^~_¥a-zA-Z0-9.'";\s,\+\-£\/@!\?%\*:$#\[\]|=\\&\u0152\u0153\u017D\u0161\u017E\u0178\u20AC]{1,50}$/
when 'HPP_BILLING_CITY', 'HPP_SHIPPING_CITY' then /^[ÀÁÂÃÂÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåªæçèéêëìíîïðñòóôõöº÷ø¤ùúûüýþÿ~L~N~Z~\~^~_¥a-zA-Z0-9.'";\s,\+\-£\/@!\?%\*:$#\[\]|=\\&\u0152\u0153\u017D\u0161\u017E\u0178\u20AC]{1,40}$/
when 'HPP_BILLING_COUNTRY', 'HPP_SHIPPING_COUNTRY' then /^([0-9])*$/
when 'HPP_BILLING_POSTALCODE', 'HPP_SHIPPING_POSTALCODE' then /^[a-zA-Z0-9\-\s]{1,16}$/
when 'HPP_BILLING_STATE', 'HPP_SHIPPING_STATE' then /^([A-Z])*$/
end
end
def get_message(key)
case key
when 'HPP_CUSTOMER_EMAIL' then 'Invalid E-mail address.'
when 'HPP_CUSTOMER_PHONENUMBER_MOBILE' then 'Invalid Telephone. The selected payment method only allows numbers, spaces or punctuation (+, |), and no more than 19 characters.'
when 'HPP_BILLING_STREET1', 'HPP_SHIPPING_STREET1', 'HPP_BILLING_STREET2', 'HPP_SHIPPING_STREET2' then 'Invalid Street address. The selected payment method only allows letters, numbers, spaces or punctuation, and no more than 50 characters per line.'
when 'HPP_BILLING_CITY', 'HPP_SHIPPING_CITY' then 'Invalid City. The selected payment method only allows letters, numbers, spaces or punctuation, and no more than 40 characters.'
when 'HPP_BILLING_COUNTRY', 'HPP_SHIPPING_COUNTRY' then 'Invalid Country code.'
when 'HPP_BILLING_POSTALCODE', 'HPP_SHIPPING_POSTALCODE' then 'Invalid Zip/Postal Code. The selected payment method only allows letters, numbers, spaces or punctuation, and no more than 16 characters.'
when 'HPP_BILLING_STATE', 'HPP_SHIPPING_STATE' then 'Invalid State.'
end
end
def validate(key, value)
pattern = get_pattern(key)
return value unless pattern.present?
if value =~pattern
return value
else
raise ArgumentError, get_message(key)
end
end
def add_field(name, value)
return if name.blank? || value.blank?
@fields[name.to_s] = validate(name.to_s, value.to_s)
end
end
class Helper < OffsitePayments::Helper
include Common
self.country_format = :numeric
def initialize(order, account, options = {})
@timestamp = Time.now.strftime('%Y%m%d%H%M%S')
@currency = options[:currency]
@merchant_id = account
@sub_account = options[:credential2]
@secret = options[:credential3]
super
# Credentials
add_field 'MERCHANT_ID', @merchant_id
add_field 'ACCOUNT', @sub_account
# Defaults
add_field 'AUTO_SETTLE_FLAG', '1'
add_field 'RETURN_TSS', '1'
add_field 'TIMESTAMP', @timestamp
add_field 'HPP_VERSION', '2'
# Realex does not send back CURRENCY param in response
# however it does echo any other param so we send it twice.
add_field 'X-CURRENCY', @currency
add_field 'X-TEST', @test.to_s
add_field 'ORDER_ID', "#{order}#{@timestamp.to_i}"
add_field 'COMMENT1', application_id
end
def form_fields
sign_fields
end
def amount=(amount)
add_field 'AMOUNT', format_amount(amount, @currency)
end
def billing_address(params={})
country = params[:country]
country_code = lookup_country_code(country, :alpha2)
avs_code = extract_avs_code(params)
params[:state] = lookup_state_code(country_code, params[:state])
super
add_field(mappings[:billing_address][:country], lookup_country_code(country))
add_field(mappings[:billing_address][:code], avs_code)
unless ['US', 'CA'].include?(country_code)
# HPP_BILLING_STATE is required only for US and CA, otherwise is deleted
@fields.delete_if do |k, _|
k == 'HPP_BILLING_STATE'
end
end
unless ['US', 'CA', 'GB'].include?(country_code)
# BILLING_CODE is required only for US, CA and GB, otherwise is nil,
# therefore the field is deleted for the other countries
@fields.delete_if do |k, _|
k == 'BILLING_CODE'
end
end
if @fields[mappings[:customer][:phone]]
add_field(mappings[:customer][:phone], format_phone_number(@phone_number, country_code))
end
end
def shipping_address(params={})
country = params[:country]
country_code = lookup_country_code(country, :alpha2)
params[:state] = lookup_state_code(country_code, params[:state])
super
add_field(mappings[:shipping_address][:country], lookup_country_code(country))
# the mapping for 'SHIPPING_CODE' field, which has the same value as the 'HPP_SHIPPING_POSTALCODE'
add_field(mappings[:shipping_address][:code], params[:zip])
unless ['US', 'CA'].include?(country_code)
# HPP_SHIPPING_STATE is required only for US and CA, otherwise is deleted
@fields.delete_if do |k, _|
k == 'HPP_SHIPPING_STATE'
end
end
if @fields[mappings[:customer][:phone]]&.[](0..1) == '0|'
add_field(mappings[:customer][:phone], format_phone_number(@phone_number, country_code))
end
end
def customer(params={})
country = @fields[mappings[:billing_address][:country]]
@phone_number = params[:phone]
params[:phone] = format_phone_number(@phone_number, lookup_country_code(country, :alpha2))
super
end
def addresses_match(address_match = nil)
return if address_match.nil?
add_field(
mappings[:addresses_match],
extract_address_match_indicator(address_match)
)
copy_billing_address if address_match
end
def comment(comment = nil)
add_field(mappings[:comment], comment)
end
# HPP does not want shipping address and HPP_ADDRESS_MATCH_INDICATOR to be sent
# if the product does not require shipping
def require_shipping(require_shipping = nil)
return unless require_shipping == false
@fields.delete_if do |k, _|
k.start_with?('HPP_SHIPPING_') || k == 'HPP_ADDRESS_MATCH_INDICATOR'
end
end
def sign_fields
@fields.merge!('SHA1HASH' => generate_signature)
end
def generate_signature
fields_to_sign = []
['TIMESTAMP', 'MERCHANT_ID', 'ORDER_ID', 'AMOUNT', 'CURRENCY'].each do |field|
fields_to_sign << @fields[field]
end
create_signature(fields_to_sign, @secret)
end
# Realex Required Fields
mapping :currency, 'CURRENCY'
mapping :order, 'CHECKOUT_ID'
mapping :amount, 'AMOUNT'
mapping :notify_url, 'MERCHANT_RESPONSE_URL'
mapping :return_url, 'MERCHANT_RETURN_URL'
# Realex Optional fields
mapping :customer, :email => 'HPP_CUSTOMER_EMAIL',
:phone => 'HPP_CUSTOMER_PHONENUMBER_MOBILE'
mapping :shipping_address, :zip => 'HPP_SHIPPING_POSTALCODE',
:country => 'HPP_SHIPPING_COUNTRY',
:address1 => 'HPP_SHIPPING_STREET1',
:address2 => 'HPP_SHIPPING_STREET2',
:address3 => 'HPP_SHIPPING_STREET3',
:city => 'HPP_SHIPPING_CITY',
:state => 'HPP_SHIPPING_STATE',
:code => 'SHIPPING_CODE'
mapping :billing_address, :zip => 'HPP_BILLING_POSTALCODE',
:country => 'HPP_BILLING_COUNTRY',
:address1 => 'HPP_BILLING_STREET1',
:address2 => 'HPP_BILLING_STREET2',
:address3 => 'HPP_BILLING_STREET3',
:city => 'HPP_BILLING_CITY',
:state => 'HPP_BILLING_STATE',
:code => 'BILLING_CODE'
mapping :addresses_match, 'HPP_ADDRESS_MATCH_INDICATOR'
mapping :comment, 'COMMENT2'
end
class Notification < OffsitePayments::Notification
include Common
def initialize(post, options={})
super
@secret = options[:credential3]
end
# Required Notification methods to define
def acknowledge(authcode = nil)
verified?
end
def item_id
checkout_id
end
def transaction_id
pasref
end
def test?
params['X-TEST']
end
def status
if result == '00'
'Completed'
else
'Invalid'
end
end
# Realex does not send back the currency param by default
# we have sent this additional parameter
def currency
params['X-CURRENCY']
end
def gross
format_amount_as_float(params['AMOUNT'], currency)
end
def complete?
verified? && status == 'Completed'
end
# Fields for Realex signature verification
def timestamp
params['TIMESTAMP']
end
def merchant_id
params['MERCHANT_ID']
end
def checkout_id
params['CHECKOUT_ID']
end
def order_id
params['ORDER_ID']
end
def result
params['RESULT']
end
def message
params['MESSAGE']
end
def authcode
params['AUTHCODE']
end
def signature
params['SHA1HASH']
end
def calculated_signature
fields = [timestamp, merchant_id, order_id, result, message, pasref, authcode]
create_signature(fields, @secret)
end
def verified?
signature == calculated_signature
end
# Extra data (available from Realex)
def cvn_result
params['CVNRESULT']
end
def avs_postcode_result
params['AVSPOSTCODERESULT']
end
def avs_address_result
params['AVSADDRESSRESULT']
end
def pasref
params['PASREF']
end
def eci
params['ECI']
end
def cavv
params['CAVV']
end
def xid
params['XID']
end
end
class Return < OffsitePayments::Return
def initialize(data, options)
super
@notification = Notification.new(data, options)
end
def success?
notification.complete?
end
# TODO: realex does not provide a separate cancelled endpoint
def cancelled?
false
end
def message
notification.message
end
end
end
end
end