module ActiveMerchant #:nodoc: module Billing #:nodoc: class GlobalCollectGateway < Gateway self.display_name = "GlobalCollect" self.homepage_url = "http://www.globalcollect.com/" self.test_url = "https://api-sandbox.globalcollect.com/" self.live_url = "https://api.globalcollect.com/" self.supported_countries = ["AD", "AE", "AG", "AI", "AL", "AM", "AO", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PL", "PN", "PS", "PT", "PW", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TC", "TD", "TG", "TH", "TJ", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VG", "VI", "VN", "WF", "WS", "ZA", "ZM", "ZW"] self.default_currency = "USD" self.money_format = :cents self.supported_cardtypes = [:visa, :master, :american_express, :discover] def initialize(options={}) requires!(options, :merchant_id, :api_key_id, :secret_api_key) super end def purchase(money, payment, options={}) MultiResponse.run do |r| r.process { authorize(money, payment, options) } r.process { capture(money, r.authorization, options) } unless capture_requested?(r) end end def authorize(money, payment, options={}) post = nestable_hash add_order(post, money, options) add_payment(post, payment, options) add_customer_data(post, options, payment) add_address(post, payment, options) add_creator_info(post, options) add_fraud_fields(post, options) commit(:authorize, post) end def capture(money, authorization, options={}) post = nestable_hash add_order(post, money, options) add_customer_data(post, options) add_creator_info(post, options) commit(:capture, post, authorization) end def refund(money, authorization, options={}) post = nestable_hash add_amount(post, money, options) add_refund_customer_data(post, options) add_creator_info(post, options) commit(:refund, post, authorization) end def void(authorization, options={}) post = nestable_hash add_creator_info(post, options) commit(:void, post, authorization) end def verify(payment, options={}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, payment, options) } r.process { void(r.authorization, options) } end end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(%r((Authorization: )[^\\]*)i, '\1[FILTERED]'). gsub(%r(("cardNumber\\":\\")\d+), '\1[FILTERED]'). gsub(%r(("cvv\\":\\")\d+), '\1[FILTERED]') end private BRAND_MAP = { "visa" => "1", "american_express" => "2", "master" => "3", "discover" => "128", "jcb" => "125", "diners_club" => "132" } def add_order(post, money, options) post["order"]["amountOfMoney"] = { "amount" => amount(money), "currencyCode" => options[:currency] || currency(money) } post["order"]["references"] = { "merchantReference" => options[:order_id], "descriptor" => options[:description] # Max 256 chars } post["order"]["references"]["invoiceData"] = { "invoiceNumber" => options[:invoice] } end def add_creator_info(post, options) post['sdkIdentifier'] = options[:sdk_identifier] if options[:sdk_identifier] post['sdkCreator'] = options[:sdk_creator] if options[:sdk_creator] post['integrator'] = options[:integrator] if options[:integrator] post['shoppingCartExtension'] = {} post['shoppingCartExtension']['creator'] = options[:creator] if options[:creator] post['shoppingCartExtension']['name'] = options[:name] if options[:name] post['shoppingCartExtension']['version'] = options[:version] if options[:version] post['shoppingCartExtension']['extensionID'] = options[:extension_ID] if options[:extension_ID] end def add_amount(post, money, options={}) post["amountOfMoney"] = { "amount" => amount(money), "currencyCode" => options[:currency] || currency(money) } end def add_payment(post, payment, options) year = format(payment.year, :two_digits) month = format(payment.month, :two_digits) expirydate = "#{month}#{year}" pre_authorization = options[:pre_authorization] ? 'PRE_AUTHORIZATION' : 'FINAL_AUTHORIZATION' post["cardPaymentMethodSpecificInput"] = { "paymentProductId" => BRAND_MAP[payment.brand], "skipAuthentication" => "true", # refers to 3DSecure "skipFraudService" => "true", "authorizationMode" => pre_authorization } post["cardPaymentMethodSpecificInput"]["card"] = { "cvv" => payment.verification_value, "cardNumber" => payment.number, "expiryDate" => expirydate, "cardholderName" => payment.name } end def add_customer_data(post, options, payment = nil) post["order"]["customer"] = { "merchantCustomerId" => options[:customer] } if payment post["order"]["customer"]["personalInformation"] = { "name" => { "firstName" => payment.first_name[0..14], "surname" => payment.last_name[0..69] } } end post["order"]["companyInformation"] = { "name" => options[:company] } post["order"]["contactDetails"] = { "emailAddress" => options[:email] } if address = options[:billing_address] || options[:address] post["order"]["contactDetails"] = { "phoneNumber" => address[:phone] } end end def add_refund_customer_data(post, options) if address = options[:billing_address] || options[:address] post["customer"]["address"] = { "countryCode" => address[:country] } post["customer"]["contactDetails"] = { "emailAddress" => options[:email], "phoneNumber" => address[:phone] } end end def add_address(post, creditcard, options) billing_address = options[:billing_address] || options[:address] shipping_address = options[:shipping_address] if billing_address = options[:billing_address] || options[:address] post["order"]["customer"]["billingAddress"] = { "street" => billing_address[:address1], "additionalInfo" => billing_address[:address2], "zip" => billing_address[:zip], "city" => billing_address[:city], "state" => billing_address[:state], "countryCode" => billing_address[:country] } end if shipping_address post["order"]["customer"]["shippingAddress"] = { "street" => shipping_address[:address1], "additionalInfo" => shipping_address[:address2], "zip" => shipping_address[:zip], "city" => shipping_address[:city], "state" => shipping_address[:state], "countryCode" => shipping_address[:country] } post["order"]["customer"]["shippingAddress"]["name"] = { "firstName" => shipping_address[:firstname], "surname" => shipping_address[:lastname] } end end def add_fraud_fields(post, options) fraud_fields = {} fraud_fields.merge!(options[:fraud_fields]) if options[:fraud_fields] fraud_fields.merge!({customerIpAddress: options[:ip]}) if options[:ip] post["fraudFields"] = fraud_fields unless fraud_fields.empty? end def parse(body) JSON.parse(body) end def url(action, authorization) (test? ? test_url : live_url) + uri(action, authorization) end def uri(action, authorization) uri = "/v1/#{@options[:merchant_id]}/" case action when :authorize uri + "payments" when :capture uri + "payments/#{authorization}/approve" when :refund uri + "payments/#{authorization}/refund" when :void uri + "payments/#{authorization}/cancel" end end def commit(action, post, authorization = nil) begin response = parse(ssl_post(url(action, authorization), post.to_json, headers(action, post, authorization))) rescue ResponseError => e if e.response.code.to_i >= 400 response = parse(e.response.body) end end succeeded = success_from(response) Response.new( succeeded, message_from(succeeded, response), response, authorization: authorization_from(succeeded, response), error_code: error_code_from(succeeded, response), test: test? ) end def headers(action, post, authorization = nil) { "Content-Type" => content_type, "Authorization" => auth_digest(action, post, authorization), "Date" => date } end def auth_digest(action, post, authorization = nil) data = <<-EOS POST #{content_type} #{date} #{uri(action, authorization)} EOS digest = OpenSSL::Digest.new('sha256') key = @options[:secret_api_key] "GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))}" end def date @date ||= Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z") # Must be same in digest and HTTP header end def content_type "application/json" end def success_from(response) !response["errorId"] && response["status"] != "REJECTED" end def message_from(succeeded, response) if succeeded "Succeeded" else if errors = response["errors"] errors.first.try(:[], "message") elsif status = response["status"] "Status: " + status else "No message available" end end end def authorization_from(succeeded, response) if succeeded response["id"] || response["payment"]["id"] || response["paymentResult"]["payment"]["id"] else response["errorId"] end end def error_code_from(succeeded, response) unless succeeded if errors = response["errors"] errors.first.try(:[], "code") elsif status = response.try(:[], "statusOutput").try(:[], "statusCode") status.to_s else "No error code available" end end end def nestable_hash Hash.new {|h,k| h[k] = Hash.new(&h.default_proc) } end def capture_requested?(response) response.params.try(:[], "payment").try(:[], "status") == "CAPTURE_REQUESTED" end end end end