require 'savon' # Note: Full data schema can be found at: # https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx?schema=schema1 # Additional Note: No idea what schema version that's for, or if it ever returns # anything except latest :( class HeartlandPortico attr_reader :credentials def initialize(credentials, test=false) @credentials = credentials @test = test end SUPPORTED_REQUEST_METHODS = %w( check_sale check_void credit_account_verify credit_add_to_batch credit_auth credit_return credit_reversal credit_sale credit_void ) SUPPORTED_REQUEST_METHODS.each do |method| define_method method do |params| client_call(method, params) end end private def camelize(string) # Couldn't get ActiveSupport to load standalone, rolling my own string.to_s.gsub(/(?:^|_)(.)/) { $1.upcase } end def client Savon.client do |savon| savon.wsdl wsdl_path savon.soap_version 2 savon.endpoint endpoint # savon.log_level :debug # savon.log true savon.convert_request_keys_to :camelcase savon.pretty_print_xml true end end def client_call(operation, params) message = xml_message(operation) do |xml| if pin_block?(operation) xml.tag!("tns:Block1") do xml_build(xml, params) end else xml_build(xml, params) end end Response.new(operation, client.call(:do_transaction, :message_tag => 'PosRequest', :message => message)) end def endpoint if @test "https://cert.api2-C.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx" else "https://api2-C.heartlandportico.com/Hps.Exchange.PosGateway/PosGatewayService.asmx" end end def pin_block?(operation) %w( check_sale check_void credit_account_verify credit_auth credit_sale credit_return credit_reversal ).include? operation.to_s end # Loaded live in production, or from a local cache for testing def wsdl_path if @test File.expand_path(File.join(__FILE__, '..', 'wsdl.xml')) else "#{endpoint}?wsdl=wsdl1" end end def xml_build(xml, values) values.keys.sort_by{|k|k.to_s}.each do |key| value = values[key] tag = "tns:#{camelize(key)}" if value.to_s.empty? # Skip empty values elsif value.kind_of? Hash xml.tag!(tag) { xml_build(xml, value) } else xml.tag!(tag, value) end end end def xml_message(operation) xml = Builder::XmlMarkup.new xml.tag!("tns:Ver1.0") do xml.tag!("tns:Header") do xml_build(xml, credentials) end xml.tag!("tns:Transaction") do xml.tag!("tns:#{camelize(operation)}") do yield xml end end end end class Response attr_reader :body, :operation def initialize(operation, savon_response) @operation = operation.to_sym @body = savon_response.body[:pos_response][:ver1_0] end def authorization body[:header][:gateway_txn_id] if success? end def message return "ERROR" unless successful_request? # CreditAddToBatch has an explicit empty response, with hard errors if not successful return "APPROVAL" if body[:transaction].has_key?(operation) and body[:transaction][operation].nil? response[:rsp_text] || response[:rsp_message] end def success? ["APPROVAL", "CARD OK", "Transaction Approved"].include? message end def successful_request? body[:header][:gateway_rsp_msg] == "Success" end def avs_code response[:avs_rslt_code] end def avs_address %w(A Y X B D M).include? avs_code end def avs_zip %w(Z W Y X D M P).include? avs_code end def cvv_code response[:cvv_rslt_code] end def cvv # Valid Values: # M: Match # N: No Match # P: Not Processed # S: Should have been present # U: Issuer not certified cvv_code == "M" end def response # Yes, I know that's a lot of edge case handling, but I _really_ # want that to be a hash. body[:transaction][operation] || {} rescue {} end alias params response end end