# encoding: utf-8 module ActiveMerchant #:nodoc: module Billing #:nodoc: class PayuInGateway < Gateway self.test_url = 'https://test.payu.in/_payment' self.live_url = 'https://secure.payu.in/_payment' TEST_INFO_URL = 'https://test.payu.in/merchant/postservice.php?form=2' LIVE_INFO_URL = 'https://info.payu.in/merchant/postservice.php?form=2' self.supported_countries = ['IN'] self.default_currency = 'INR' self.supported_cardtypes = %i[visa master american_express diners_club maestro] self.homepage_url = 'https://www.payu.in/' self.display_name = 'PayU India' def initialize(options = {}) requires!(options, :key, :salt) super end def purchase(money, payment, options = {}) requires!(options, :order_id) post = {} add_invoice(post, money, options) add_payment(post, payment) add_addresses(post, options) add_customer_data(post, options) add_auth(post) MultiResponse.run do |r| r.process { commit(url('purchase'), post) } if r.params['enrolled'].to_s == '0' r.process { commit(r.params['post_uri'], r.params['form_post_vars']) } else r.process { handle_3dsecure(r) } end end end def refund(money, authorization, options = {}) raise ArgumentError, 'Amount is required' unless money post = {} post[:command] = 'cancel_refund_transaction' post[:var1] = authorization post[:var2] = generate_unique_id post[:var3] = amount(money) add_auth(post, :command, :var1) commit(url('refund'), post) end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(/(ccnum=)[^&\n"]*(&|\n|"|$)/, '\1[FILTERED]\2'). gsub(/(ccvv=)[^&\n"]*(&|\n|"|$)/, '\1[FILTERED]\2'). gsub(/(card_hash=)[^&\n"]*(&|\n|"|$)/, '\1[FILTERED]\2'). gsub(/(ccnum":")[^"]*(")/, '\1[FILTERED]\2'). gsub(/(ccvv":")[^"]*(")/, '\1[FILTERED]\2') end private PAYMENT_DIGEST_KEYS = %w( txnid amount productinfo firstname email udf1 udf2 udf3 udf4 udf5 bogus bogus bogus bogus bogus ) def add_auth(post, *digest_keys) post[:key] = @options[:key] post[:txn_s2s_flow] = 1 digest_keys = PAYMENT_DIGEST_KEYS if digest_keys.empty? digest = Digest::SHA2.new(512) digest << @options[:key] << '|' digest_keys.each do |key| digest << (post[key.to_sym] || '') << '|' end digest << @options[:salt] post[:hash] = digest.hexdigest end def add_customer_data(post, options) post[:email] = clean(options[:email] || 'unknown@example.com', nil, 50) post[:phone] = clean((options[:billing_address] && options[:billing_address][:phone]) || '11111111111', :numeric, 50) end def add_addresses(post, options) if options[:billing_address] post[:address1] = clean(options[:billing_address][:address1], :text, 100) post[:address2] = clean(options[:billing_address][:address2], :text, 100) post[:city] = clean(options[:billing_address][:city], :text, 50) post[:state] = clean(options[:billing_address][:state], :text, 50) post[:country] = clean(options[:billing_address][:country], :text, 50) post[:zipcode] = clean(options[:billing_address][:zip], :numeric, 20) end if options[:shipping_address] if options[:shipping_address][:name] first, *rest = options[:shipping_address][:name].split(/\s+/) post[:shipping_firstname] = clean(first, :name, 60) post[:shipping_lastname] = clean(rest.join(' '), :name, 20) end post[:shipping_address1] = clean(options[:shipping_address][:address1], :text, 100) post[:shipping_address2] = clean(options[:shipping_address][:address2], :text, 100) post[:shipping_city] = clean(options[:shipping_address][:city], :text, 50) post[:shipping_state] = clean(options[:shipping_address][:state], :text, 50) post[:shipping_country] = clean(options[:shipping_address][:country], :text, 50) post[:shipping_zipcode] = clean(options[:shipping_address][:zip], :numeric, 20) post[:shipping_phone] = clean(options[:shipping_address][:phone], :numeric, 50) end end def add_invoice(post, money, options) post[:amount] = amount(money) post[:txnid] = clean(options[:order_id], :alphanumeric, 30) post[:productinfo] = clean(options[:description] || 'Purchase', nil, 100) post[:surl] = 'http://example.com' post[:furl] = 'http://example.com' end BRAND_MAP = { visa: 'VISA', master: 'MAST', american_express: 'AMEX', diners_club: 'DINR', maestro: 'MAES' } def add_payment(post, payment) post[:pg] = 'CC' post[:firstname] = clean(payment.first_name, :name, 60) post[:lastname] = clean(payment.last_name, :name, 20) post[:bankcode] = BRAND_MAP[payment.brand.to_sym] post[:ccnum] = payment.number post[:ccvv] = payment.verification_value post[:ccname] = payment.name post[:ccexpmon] = format(payment.month, :two_digits) post[:ccexpyr] = format(payment.year, :four_digits) end def clean(value, format, maxlength) value ||= '' value = case format when :alphanumeric value.gsub(/[^A-Za-z0-9]/, '') when :name value.gsub(/[^A-Za-z ]/, '') when :numeric value.gsub(/[^0-9]/, '') when :text value.gsub(/[^A-Za-z0-9@\-_\/\. ]/, '') when nil value else raise "Unknown format #{format} for #{value}" end value[0...maxlength] end def parse(body) top = JSON.parse(body) if result = top.delete('result') result.split('&').inject({}) do |hash, string| key, value = string.split('=') hash[CGI.unescape(key).downcase] = CGI.unescape(value || '') hash end.each do |key, value| if top[key] top["result_#{key}"] = value else top[key] = value end end end if response = top.delete('response') top.merge!(response) end top rescue JSON::ParserError { 'error' => "Invalid response received from the PayU API. (The raw response was `#{body}`)." } end def commit(url, parameters) response = parse(ssl_post(url, post_data(parameters), 'Accept-Encoding' => 'identity')) Response.new( success_from(response), message_from(response), response, authorization: authorization_from(response), test: test? ) end def url(action) case action when 'purchase' (test? ? test_url : live_url) else (test? ? TEST_INFO_URL : LIVE_INFO_URL) end end def success_from(response) if response['result_status'] (response['status'] == 'success' && response['result_status'] == 'success') else (response['status'] == 'success' || response['status'].to_s == '1') end end def message_from(response) (response['error_message'] || response['error'] || response['msg']) end def authorization_from(response) response['mihpayid'] end def post_data(parameters = {}) PostData.new.merge!(parameters).to_post_data end def handle_3dsecure(response) Response.new(false, '3D-secure enrolled cards are not supported.') end end end end