From dbc710f7f0e846960fc0cdc95d1b80bc8ce98fe1 Mon Sep 17 00:00:00 2001 From: Duff OMelia Date: Tue, 21 Apr 2015 17:47:47 -0400 Subject: [PATCH] Ezic: Add gateway --- CHANGELOG | 1 + README.md | 1 + .../billing/gateways/authorize_net.rb | 1 + lib/active_merchant/billing/gateways/ezic.rb | 193 ++++++++++++++++++ test/fixtures.yml | 4 + test/remote/gateways/remote_ezic_test.rb | 110 ++++++++++ test/unit/gateways/ezic_test.rb | 167 +++++++++++++++ 7 files changed, 477 insertions(+) create mode 100644 lib/active_merchant/billing/gateways/ezic.rb create mode 100644 test/remote/gateways/remote_ezic_test.rb create mode 100644 test/unit/gateways/ezic_test.rb diff --git a/CHANGELOG b/CHANGELOG index dfd6aa6fe21..ee8b3169541 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ * Braintree: Add service_fee_amount option [duff] * SecureNet: Allow shipping_address[:name] [duff] * MonerisUS: Add verify [mrezentes] +* Ezic: Add gateway [duff] == Version 1.48.0 (April 8, 2015) diff --git a/README.md b/README.md index f65836fe264..7cc08bdb5d8 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ The [ActiveMerchant Wiki](http://github.com/Shopify/active_merchant/wikis) conta * [eWAY](http://www.eway.com.au/) - AU, NZ, GB * [eWAY Rapid](http://www.eway.com.au/) - AU, NZ, GB, SG * [E-xact](http://www.e-xact.com) - CA, US +* [Ezic](http://www.ezic.com/) - AU, CA, CN, FR, DE, GI, IL, MT, MU, MX, NL, NZ, PA, PH, RU, SG, KR, ES, KN, GB * [Fat Zebra](https://www.fatzebra.com.au/) - AU * [Federated Canada](http://www.federatedcanada.com/) - CA * [Finansbank WebPOS](https://www.fbwebpos.com/) - US, TR diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index c0e570436ed..90843867888 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -486,6 +486,7 @@ def fraud_review?(response) (response[:response_code] == FRAUD_REVIEW) end + def using_live_gateway_in_test_mode?(response) !test? && response[:test_request] == "1" end diff --git a/lib/active_merchant/billing/gateways/ezic.rb b/lib/active_merchant/billing/gateways/ezic.rb new file mode 100644 index 00000000000..e5d4b62f0e1 --- /dev/null +++ b/lib/active_merchant/billing/gateways/ezic.rb @@ -0,0 +1,193 @@ +module ActiveMerchant + module Billing + class EzicGateway < Gateway + self.live_url = 'https://secure-dm3.ezic.com/gw/sas/direct3.2' + + self.supported_countries = %w(AU CA CN FR DE GI IL MT MU MX NL NZ PA PH RU SG KR ES KN GB) + self.default_currency = 'USD' + self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club] + + self.homepage_url = 'http://www.ezic.com/' + self.display_name = 'Ezic' + + def initialize(options={}) + requires!(options, :account_id) + super + end + + def purchase(money, payment, options={}) + post = {} + + add_account_id(post) + add_invoice(post, money, options) + add_payment(post, payment) + add_customer_data(post, options) + + commit("S", post) + end + + def authorize(money, payment, options={}) + post = {} + + add_account_id(post) + add_invoice(post, money, options) + add_payment(post, payment) + add_customer_data(post, options) + + commit("A", post) + end + + def capture(money, authorization, options={}) + post = {} + + add_account_id(post) + add_invoice(post, money, options) + add_authorization(post, authorization) + add_pay_type(post) + + commit("D", post) + end + + def refund(money, authorization, options={}) + post = {} + + add_account_id(post) + add_invoice(post, money, options) + add_authorization(post, authorization) + add_pay_type(post) + + commit("R", post) + end + + def verify(credit_card, options={}) + authorize(100, credit_card, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((card_number=)\w+), '\1[FILTERED]'). + gsub(%r((card_cvv2=)\w+), '\1[FILTERED]') + end + + private + + def add_account_id(post) + post[:account_id] = @options[:account_id] + end + + def add_addresses(post, options) + add_billing_address(post, options) + add_shipping_address(post, options) + end + + def add_billing_address(post, options) + address = options[:billing_address] || {} + + if address[:name] + names = address[:name].split + post[:bill_name2] = names.pop + post[:bill_name1] = names.join(" ") + end + + post[:bill_street] = address[:address1] if address[:address1] + post[:bill_city] = address[:city] if address[:city] + post[:bill_state] = address[:state] if address[:state] + post[:bill_zip] = address[:zip] if address[:zip] + post[:bill_country] = address[:country] if address[:country] + post[:cust_phone] = address[:phone] if address[:phone] + end + + def add_shipping_address(post, options) + address = options[:shipping_address] || {} + + if address[:name] + names = address[:name].split + post[:ship_name2] = names.pop + post[:ship_name1] = names.join(" ") + end + + post[:ship_street] = address[:address1] if address[:address1] + post[:ship_city] = address[:city] if address[:city] + post[:ship_state] = address[:state] if address[:state] + post[:ship_zip] = address[:zip] if address[:zip] + post[:ship_country] = address[:country] if address[:country] + end + + def add_customer_data(post, options) + post[:cust_ip] = options[:ip] if options[:ip] + post[:cust_email] = options[:email] if options[:email] + add_addresses(post, options) + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:description] = options[:description] if options[:description] + end + + def add_payment(post, payment) + add_pay_type(post) + post[:card_number] = payment.number + post[:card_cvv2] = payment.verification_value + post[:card_expire] = expdate(payment) + end + + def add_authorization(post, authorization) + post[:orig_id] = authorization + end + + def add_pay_type(post) + post[:pay_type] = "C" + end + + def parse(body) + CGI::parse(body).inject({}) { |hash, (key, value)| hash[key] = value.first; hash } + end + + def commit(transaction_type, parameters) + parameters[:tran_type] = transaction_type + + begin + response = parse(ssl_post(live_url, post_data(parameters), headers)) + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response["avs_code"]), + cvv_result: CVVResult.new(response["cvv2_code"]), + test: test? + ) + rescue ResponseError => e + Response.new(false, e.response.message) + end + end + + def success_from(response) + response["status_code"] == "1" || response["status_code"] == "T" + end + + def message_from(response) + response["auth_msg"] + end + + def authorization_from(response) + response["trans_id"] + end + + def post_data(parameters = {}) + parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&") + end + + def headers + { + "User-Agent" => "ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + } + end + end + + end +end diff --git a/test/fixtures.yml b/test/fixtures.yml index 12dcea9d30f..267813ebaf6 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -201,6 +201,10 @@ exact: login: "A00427-01" password: testus +# Working credentials, no need to replace +ezic: + account_id: "120536457270" + # Working credentials, no need to replace fat_zebra: username: TEST diff --git a/test/remote/gateways/remote_ezic_test.rb b/test/remote/gateways/remote_ezic_test.rb new file mode 100644 index 00000000000..3509b83de3b --- /dev/null +++ b/test/remote/gateways/remote_ezic_test.rb @@ -0,0 +1,110 @@ +require 'test_helper' + +class RemoteEzicTest < Test::Unit::TestCase + def setup + @gateway = EzicGateway.new(fixtures(:ezic)) + + @amount = 100 + @failed_amount = 19088 + @credit_card = credit_card('4000100011112224') + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal "TEST APPROVED", response.message + end + + def test_failed_purchase + response = @gateway.purchase(@failed_amount, @credit_card, @options) + assert_failure response + assert_equal "TEST DECLINED", response.message + end + + def test_successful_authorize_and_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount, auth.authorization) + assert_success capture + assert_equal "TEST CAPTURED", capture.message + end + + def test_failed_authorize + response = @gateway.authorize(@failed_amount, @credit_card, @options) + assert_failure response + assert_equal "TEST DECLINED", response.message + end + + def test_failed_capture + auth = @gateway.authorize(@amount, @credit_card, @options) + assert_success auth + + assert capture = @gateway.capture(@amount+30, auth.authorization) + assert_failure capture + assert_match /Settlement amount cannot exceed authorized amount/, capture.message + end + + def test_successful_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount, purchase.authorization) + assert_success refund + assert_equal "TEST RETURNED", refund.message + end + + def test_partial_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount-1, purchase.authorization) + assert_success refund + assert_equal "TEST RETURNED", refund.message + assert_equal "-0.99", refund.params["settle_amount"] + end + + def test_failed_refund + purchase = @gateway.purchase(@amount, @credit_card, @options) + assert_success purchase + + assert refund = @gateway.refund(@amount + 49, purchase.authorization) + assert_failure refund + assert_match /Amount of refunds exceed original sale/, refund.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal "TEST APPROVED", response.message + end + + def test_failed_verify + response = @gateway.verify(credit_card(""), @options) + assert_failure response + assert_match %r{Missing card or check number}, response.message + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @credit_card, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@credit_card.number, transcript) + assert_scrubbed(@credit_card.verification_value, transcript) + end + + def test_invalid_login + gateway = EzicGateway.new(account_id: '11231') + response = gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match /Invalid account number/, response.message + end +end diff --git a/test/unit/gateways/ezic_test.rb b/test/unit/gateways/ezic_test.rb new file mode 100644 index 00000000000..2288ceba087 --- /dev/null +++ b/test/unit/gateways/ezic_test.rb @@ -0,0 +1,167 @@ +require 'test_helper' + +class EzicTest < Test::Unit::TestCase + def setup + @gateway = EzicGateway.new(account_id: 'TheID') + @credit_card = credit_card + @amount = 100 + + @options = { + order_id: '1', + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_purchase + @gateway.expects(:ssl_post).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '120741089764', response.authorization + assert response.test? + assert_equal "Street address and 9-digit postal code match.", response.avs_result["message"] + assert_equal "CVV matches", response.cvv_result["message"] + end + + def test_failed_purchase + @gateway.expects(:ssl_post).returns(failed_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_equal "TEST DECLINED", response.message + end + + def test_successful_authorize + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_equal '120762306743', response.authorization + end + + def test_failed_authorize + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert_equal "TEST DECLINED", response.message + end + + def test_successful_capture + @gateway.expects(:ssl_post).returns(successful_capture_response) + + response = @gateway.capture(@amount, "123312") + assert_success response + assert_equal '120762306743', response.authorization + end + + def test_failed_capture + @gateway.expects(:raw_ssl_request).returns(failed_capture_response) + + response = @gateway.capture(@amount, "2131212") + assert_failure response + assert_equal "20105: Settlement amount cannot exceed authorized amount", response.message + end + + def test_successful_refund + @gateway.expects(:ssl_post).returns(successful_refund_response) + + response = @gateway.refund(@amount, "32432423", @options) + assert_success response + assert_equal '120421340652', response.authorization + end + + def test_failed_refund + @gateway.expects(:raw_ssl_request).returns(failed_refund_response) + + response = @gateway.refund(@amount, "5511231") + assert_failure response + assert_equal "20183: Amount of refunds exceed original sale", response.message + end + + def test_successful_verify + @gateway.expects(:ssl_post).returns(successful_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_equal '120762306743', response.authorization + end + + def test_failed_verify + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.verify(@credit_card, @options) + assert_failure response + assert_equal "TEST DECLINED", response.message + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + <- "account_id=120536457270&amount=1.00&description=Store+Purchase&pay_type=C&card_number=4000100011112224&card_cvv2=123&card_expire=0916&bill_name2=Smith&bill_name1=Jim&bill_street=1234+My+Street&bill_city=Ottawa&bill_state=ON&bill_zip=K1C2N6&bill_country=CA&cust_phone=%28555%29555-5555&tran_type=S" + -> "avs_code=X&cvv2_code=M&status_code=1&processor=TEST&auth_code=999999&settle_amount=1.00&settle_currency=USD&trans_id=120477042083&auth_msg=TEST+APPROVED&auth_date=2015-04-22+15:20:05" + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + <- "account_id=120536457270&amount=1.00&description=Store+Purchase&pay_type=C&card_number=[FILTERED]&card_cvv2=[FILTERED]&card_expire=0916&bill_name2=Smith&bill_name1=Jim&bill_street=1234+My+Street&bill_city=Ottawa&bill_state=ON&bill_zip=K1C2N6&bill_country=CA&cust_phone=%28555%29555-5555&tran_type=S" + -> "avs_code=X&cvv2_code=M&status_code=1&processor=TEST&auth_code=999999&settle_amount=1.00&settle_currency=USD&trans_id=120477042083&auth_msg=TEST+APPROVED&auth_date=2015-04-22+15:20:05" + POST_SCRUBBED + end + + def successful_purchase_response + "avs_code=X&cvv2_code=M&status_code=1&processor=TEST&auth_code=999999&settle_amount=1.00&settle_currency=USD&trans_id=120741089764&auth_msg=TEST+APPROVED&auth_date=2015-04-23+15:27:28" + end + + def failed_purchase_response + "avs_code=Y&cvv2_code=M&status_code=0&processor=TEST&settle_currency=USD&settle_amount=190.88&trans_id=120740287652&auth_msg=TEST+DECLINED&auth_date=2015-04-23+15:31:30" + end + + def successful_authorize_response + "avs_code=X&cvv2_code=M&status_code=T&processor=TEST&auth_code=999999&settle_amount=1.00&settle_currency=USD&trans_id=120762306743&auth_msg=TEST+APPROVED&ticket_code=XXXXXXXXXXXXXXX&auth_date=2015-04-23+17:24:37" + end + + def failed_authorize_response + "avs_code=Y&cvv2_code=M&status_code=0&processor=TEST&auth_code=999999&settle_currency=USD&settle_amount=190.88&trans_id=120761061862&auth_msg=TEST+DECLINED&ticket_code=XXXXXXXXXXXXXXX&auth_date=2015-04-23+17:25:35" + end + + def successful_capture_response + "avs_code=X&cvv2_code=M&status_code=1&auth_code=999999&trans_id=120762306743&auth_msg=TEST+CAPTURED&ticket_code=XXXXXXXXXXXXXXX&auth_date=2015-04-23+17:24:37" + end + + def failed_capture_response + MockResponse.failed("20105: Settlement amount cannot exceed authorized amount") + end + + def successful_refund_response + "status_code=1&processor=TEST&auth_code=RRRRRR&settle_amount=-1.00&settle_currency=USD&trans_id=120421340652&auth_msg=TEST+RETURNED&auth_date=2015-04-23+18:26:02" + end + + def failed_refund_response + MockResponse.failed("20183: Amount of refunds exceed original sale") + end + + class MockResponse + attr_reader :code, :message + def self.succeeded(message) + MockResponse.new(200, message) + end + + def self.failed(message) + MockResponse.new(611, message) + end + + def initialize(code, message) + @code, @message = code, message + end + end + +end