module ActiveMerchant #:nodoc: module Billing #:nodoc: class JetpayGateway < Gateway class_attribute :live_us_url, :live_ca_url self.test_url = 'https://test1.jetpay.com/jetpay' self.live_us_url = 'https://gateway17.jetpay.com/jetpay' self.live_ca_url = 'https://gateway17.jetpay.com/canada-bb' # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = %w[US CA] # The card types supported by the payment gateway self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.jetpay.com/' # The name of the gateway self.display_name = 'JetPay' # all transactions are in cents self.money_format = :cents ACTION_CODE_MESSAGES = { '000' => 'Approved.', '001' => 'Refer to card issuer.', '002' => 'Refer to card issuer, special condition.', '003' => 'Invalid merchant or service provider.', '004' => 'Pick up card.', '005' => 'Do not honor.', '006' => 'Error.', '007' => 'Pick up card, special condition.', '008' => 'Honor with ID (Show ID).', '010' => 'Partial approval.', '011' => 'VIP approval.', '012' => 'Invalid transaction.', '013' => 'Invalid amount or exceeds maximum for card program.', '014' => 'Invalid account number (no such number).', '015' => 'No such issuer.', '019' => 'Re-enter Transaction.', '021' => 'No action taken (unable to back out prior transaction).', '025' => 'Transaction Not Found.', '027' => 'File update field edit error.', '028' => 'File is temporarily unavailable.', '030' => 'Format error.', '039' => 'No credit account.', '041' => 'Pick up card (lost card).', '043' => 'Pick up card (stolen card).', '051' => 'Insufficient funds.', '052' => 'No checking account.', '053' => 'No savings account.', '054' => 'Expired Card.', '055' => 'Incorrect PIN.', '057' => 'Transaction not permitted to cardholder.', '058' => 'Transaction not allowed at terminal.', '061' => 'Exceeds withdrawal limit.', '062' => 'Restricted card (eg, Country Exclusion).', '063' => 'Security violation.', '065' => 'Activity count limit exceeded.', '068' => 'Response late.', '070' => 'Contact card issuer.', '071' => 'PIN not changed.', '075' => 'Allowable number of PIN-entry tries exceeded.', '076' => 'Unable to locate previous message (no matching retrieval reference number).', '077' => 'Repeat or reversal data are inconsistent with original message.', '078' => 'Blocked (first use), or non-existent account.', '079' => 'Key exchange validation failed.', '080' => 'Credit issuer unavailable or invalid date.', '081' => 'PIN cryptographic error found.', '082' => 'Negative online CVV results.', '084' => 'Invalid auth life cycle.', '085' => 'No reason to decline - CVV or AVS approved.', '086' => 'Cannot verify PIN.', '087' => 'Cashback not allowed.', '089' => 'Issuer Down.', '091' => 'Issuer Down.', '092' => 'Unable to route transaction.', '093' => 'Transaction cannot be completed - violation of law.', '094' => 'Duplicate transmission.', '096' => 'System error.', '100' => 'Deny.', '101' => 'Expired Card.', '103' => 'Deny - Invalid manual Entry 4DBC.', '104' => 'Deny - New card issued.', '105' => 'Deny - Account Cancelled.', '106' => 'Exceeded PIN Attempts.', '107' => 'Please Call Issuer.', '109' => 'Invalid merchant.', '110' => 'Invalid amount.', '111' => 'Invalid account.', '115' => 'Service not permitted.', '122' => 'Invalid card (CID) security code.', '125' => 'Invalid effective date.', '181' => 'Format error.', '182' => 'Please wait.', '183' => 'Invalid currency code.', '187' => 'Deny - new card issued.', '188' => 'Deny - Expiration date required.', '189' => 'Deny - Cancelled or Closed Merchant/SE.', '200' => 'Deny - Pick up card.', '400' => 'Reversal accepted.', '601' => 'Reject - EMV Chip Declined Transaction.', '602' => 'Reject - Suspected Fraud.', '603' => 'Reject - Communications Error.', '604' => 'Reject - Insufficient Approval.', '750' => 'Velocity Check Fail.', '899' => 'Misc Decline.', '900' => 'Invalid Message Type.', '901' => 'Invalid Merchant ID.', '903' => 'Debit not supported.', '904' => 'Private label not supported.', '905' => 'Invalid card type.', '906' => 'Unit not active.', '908' => 'Manual card entry invalid.', '909' => 'Invalid track information.', '911' => 'Master merchant not found.', '912' => 'Invalid card format.', '913' => 'Invalid card type.', '914' => 'Invalid card length.', '917' => 'Expired card.', '919' => 'Invalid entry type.', '920' => 'Invalid amount.', '921' => 'Invalid messge format.', '923' => 'Invalid ABA.', '924' => 'Invalid DDA.', '925' => 'Invalid TID.', '926' => 'Invalid Password.', '930' => 'Invalid zipcode.', '931' => 'Invalid Address.', '932' => 'Invalid ZIP and Address.', '933' => 'Invalid CVV2.', '934' => 'Program Not Allowed.', '940' => 'Record Not Found.', '941' => 'Merchant ID error.', '942' => 'Refund Not Allowed.', '943' => 'Refund denied.', '955' => 'Invalid PIN block.', '956' => 'Invalid KSN.', '958' => 'Bad Status.', '959' => 'Seek Record limit exceeded.', '962' => 'Invalid PIN key (Unknown KSN).', '981' => 'Invalid AVS.', '987' => 'Issuer Unavailable.', '988' => 'System error SD.', '989' => 'Database Error.', '992' => 'Transaction Timeout.', '996' => 'Bad Terminal ID.', '997' => 'Message rejected by association.', '999' => 'Communication failure', nil => 'No response returned (missing credentials?).' } def initialize(options = {}) requires!(options, :login) super end def purchase(money, credit_card, options = {}) commit(money, build_sale_request(money, credit_card, options)) end def authorize(money, credit_card, options = {}) commit(money, build_authonly_request(money, credit_card, options)) end def capture(money, reference, options = {}) split_authorization = reference.split(';') transaction_id = split_authorization[0] token = split_authorization[3] commit(money, build_capture_request(transaction_id, money, options), token) end def void(reference, options = {}) transaction_id, approval, amount, token = reference.split(';') commit(amount.to_i, build_void_request(amount.to_i, transaction_id, approval, token, options), token) end def credit(money, transaction_id_or_card, options = {}) if transaction_id_or_card.is_a?(String) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE refund(money, transaction_id_or_card, options) else commit(money, build_credit_request('CREDIT', money, nil, transaction_id_or_card, nil, options)) end end def refund(money, reference, options = {}) split_authorization = reference.split(';') transaction_id = split_authorization[0] token = split_authorization[3] credit_card = options[:credit_card] commit(money, build_credit_request('CREDIT', money, transaction_id, credit_card, token, options)) end def supports_scrubbing true end def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((>)\d+()), '\1[FILTERED]\2'). gsub(%r(()\d+()), '\1[FILTERED]\2') end private def build_xml_request(transaction_type, options = {}, transaction_id = nil, &block) xml = Builder::XmlMarkup.new xml.tag! 'JetPay' do # The basic values needed for any request xml.tag! 'TerminalID', @options[:login] xml.tag! 'TransactionType', transaction_type xml.tag! 'TransactionID', transaction_id.nil? ? generate_unique_id.slice(0, 18) : transaction_id xml.tag! 'Origin', options[:origin] if options && options[:origin] if block_given? yield xml else xml.target! end end end def build_sale_request(money, credit_card, options) build_xml_request('SALE', options) do |xml| add_credit_card(xml, credit_card) add_addresses(xml, options) add_customer_data(xml, options) add_invoice_data(xml, options) add_user_defined_fields(xml, options) xml.tag! 'TotalAmount', amount(money) xml.target! end end def build_authonly_request(money, credit_card, options) build_xml_request('AUTHONLY') do |xml| add_credit_card(xml, credit_card) add_addresses(xml, options) add_customer_data(xml, options) add_invoice_data(xml, options) add_user_defined_fields(xml, options) xml.tag! 'TotalAmount', amount(money) xml.target! end end def build_capture_request(transaction_id, money, options) build_xml_request('CAPT', options, transaction_id) do |xml| xml.tag! 'TotalAmount', amount(money) add_user_defined_fields(xml, options) end end def build_void_request(money, transaction_id, approval, token, options) build_xml_request('VOID', options, transaction_id) do |xml| xml.tag! 'Approval', approval xml.tag! 'TotalAmount', amount(money) xml.tag! 'Token', token if token xml.target! end end # `transaction_id` may be nil for unlinked credit transactions. def build_credit_request(transaction_type, money, transaction_id, card, token, options) build_xml_request(transaction_type, options, transaction_id) do |xml| add_credit_card(xml, card) if card add_invoice_data(xml, options) add_addresses(xml, options) add_customer_data(xml, options) add_user_defined_fields(xml, options) xml.tag! 'TotalAmount', amount(money) xml.tag! 'Token', token if token xml.target! end end def commit(money, request, token = nil) response = parse(ssl_post(url, request)) success = success?(response) Response.new(success, success ? 'APPROVED' : message_from(response), response, test: test?, authorization: authorization_from(response, money, token), avs_result: { code: response[:avs] }, cvv_result: response[:cvv2]) end def url live_url = @options[:region] == 'CA' ? live_ca_url : live_us_url test? ? test_url : live_url end def parse(body) return {} if body.blank? xml = REXML::Document.new(body) response = {} xml.root.elements.to_a.each do |node| parse_element(response, node) end response end def parse_element(response, node) if node.has_elements? node.elements.each { |element| parse_element(response, element) } else response[node.name.underscore.to_sym] = node.text end end def format_exp(value) format(value, :two_digits) end def success?(response) response[:action_code] == '000' end def message_from(response) ACTION_CODE_MESSAGES[response[:action_code]] end def authorization_from(response, money, previous_token) original_amount = amount(money) if money [response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';') end def add_credit_card(xml, credit_card) xml.tag! 'CardNum', credit_card.number, 'Tokenize' => true xml.tag! 'CardExpMonth', format_exp(credit_card.month) xml.tag! 'CardExpYear', format_exp(credit_card.year) xml.tag! 'CardName', [credit_card.first_name, credit_card.last_name].compact.join(' ') if credit_card.first_name || credit_card.last_name xml.tag! 'CVV2', credit_card.verification_value unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0) end def add_addresses(xml, options) if billing_address = options[:billing_address] || options[:address] xml.tag! 'BillingAddress', [billing_address[:address1], billing_address[:address2]].compact.join(' ') xml.tag! 'BillingCity', billing_address[:city] xml.tag! 'BillingStateProv', billing_address[:state] xml.tag! 'BillingPostalCode', billing_address[:zip] xml.tag! 'BillingCountry', lookup_country_code(billing_address[:country]) xml.tag! 'BillingPhone', billing_address[:phone] end if shipping_address = options[:shipping_address] xml.tag! 'ShippingInfo' do xml.tag! 'ShippingName', shipping_address[:name] xml.tag! 'ShippingAddr' do xml.tag! 'Address', [shipping_address[:address1], shipping_address[:address2]].compact.join(' ') xml.tag! 'City', shipping_address[:city] xml.tag! 'StateProv', shipping_address[:state] xml.tag! 'PostalCode', shipping_address[:zip] xml.tag! 'Country', lookup_country_code(shipping_address[:country]) end end end end def add_customer_data(xml, options) xml.tag! 'Email', options[:email] if options[:email] xml.tag! 'UserIPAddress', options[:ip] if options[:ip] end def add_invoice_data(xml, options) xml.tag! 'OrderNumber', options[:order_id] if options[:order_id] xml.tag! 'TaxAmount', amount(options[:tax]) if options[:tax] end def add_user_defined_fields(xml, options) xml.tag! 'UDField1', options[:ud_field_1] if options[:ud_field_1] xml.tag! 'UDField2', options[:ud_field_2] if options[:ud_field_2] xml.tag! 'UDField3', options[:ud_field_3] if options[:ud_field_3] end def lookup_country_code(code) country = Country.find(code) rescue nil country&.code(:alpha3) end end end end