diff --git a/lib/active_merchant/billing/gateways/square.rb b/lib/active_merchant/billing/gateways/square.rb new file mode 100644 index 00000000000..dfb0e55bd4a --- /dev/null +++ b/lib/active_merchant/billing/gateways/square.rb @@ -0,0 +1,308 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class SquareGateway < Gateway + self.test_url = 'https://connect.squareupsandbox.com/v2' + self.live_url = 'https://connect.squareup.com/v2' + + self.supported_countries = %w[US CA GB AU JP] + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover jcb union_pay] + self.money_format = :cents + + self.homepage_url = 'https://squareup.com/' + self.display_name = 'Square Payments Gateway' + + CVC_CODE_TRANSLATOR = { + 'CVV_ACCEPTED' => 'M', + 'CVV_REJECTED' => 'N', + 'CVV_NOT_CHECKED' => 'P' + }.freeze + + AVS_CODE_TRANSLATOR = { + # TODO: unsure if Square does street or only postal AVS matches + 'AVS_ACCEPTED' => 'P', # 'P' => 'Postal code matches, but street address not verified.', + 'AVS_REJECTED' => 'N', # 'N' => 'Street address and postal code do not match. For American Express: Card member\'s name, street address and postal code do not match.', + 'AVS_NOT_CHECKED' => 'I' # 'I' => 'Address not verified.', + }.freeze + + DEFAULT_API_VERSION = '2020-06-25'.freeze + + STANDARD_ERROR_CODE_MAPPING = { + 'BAD_EXPIRATION' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'INVALID_ACCOUNT' => STANDARD_ERROR_CODE[:config_error], + 'CARDHOLDER_INSUFFICIENT_PERMISSIONS' => STANDARD_ERROR_CODE[:card_declined], + 'INSUFFICIENT_PERMISSIONS' => STANDARD_ERROR_CODE[:config_error], + 'INSUFFICIENT_FUNDS' => STANDARD_ERROR_CODE[:card_declined], + 'INVALID_LOCATION' => STANDARD_ERROR_CODE[:processing_error], + 'TRANSACTION_LIMIT' => STANDARD_ERROR_CODE[:card_declined], + 'CARD_EXPIRED' => STANDARD_ERROR_CODE[:expired_card], + 'CVV_FAILURE' => STANDARD_ERROR_CODE[:incorrect_cvc], + 'ADDRESS_VERIFICATION_FAILURE' => STANDARD_ERROR_CODE[:incorrect_address], + 'VOICE_FAILURE' => STANDARD_ERROR_CODE[:card_declined], + 'PAN_FAILURE' => STANDARD_ERROR_CODE[:incorrect_number], + 'EXPIRATION_FAILURE' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'INVALID_EXPIRATION' => STANDARD_ERROR_CODE[:invalid_expiry_date], + 'CARD_NOT_SUPPORTED' => STANDARD_ERROR_CODE[:processing_error], + 'INVALID_PIN' => STANDARD_ERROR_CODE[:incorrect_pin], + 'INVALID_POSTAL_CODE' => STANDARD_ERROR_CODE[:incorrect_zip], + 'CHIP_INSERTION_REQUIRED' => STANDARD_ERROR_CODE[:processing_error], + 'ALLOWABLE_PIN_TRIES_EXCEEDED' => STANDARD_ERROR_CODE[:card_declined], + 'MANUALLY_ENTERED_PAYMENT_NOT_SUPPORTED' => STANDARD_ERROR_CODE[:unsupported_feature], + 'PAYMENT_LIMIT_EXCEEDED' => STANDARD_ERROR_CODE[:processing_error], + 'GENERIC_DECLINE' => STANDARD_ERROR_CODE[:card_declined], + 'INVALID_FEES' => STANDARD_ERROR_CODE[:config_error], + 'GIFT_CARD_AVAILABLE_AMOUNT' => STANDARD_ERROR_CODE[:card_declined], + 'BAD_REQUEST' => STANDARD_ERROR_CODE[:processing_error] + }.freeze + + def initialize(options={}) + requires!(options, :access_token) + @access_token = options[:access_token] + @fee_currency = options[:fee_currency] || default_currency + super + end + + def authorize(money, payment, options={}) + post = create_post_for_auth_or_purchase(money, payment, options) + post[:autocomplete] = false + + commit(:post, 'payments', post, options) + end + + def purchase(money, payment, options={}) + post = create_post_for_auth_or_purchase(money, payment, options) + post[:autocomplete] = true + + commit(:post, 'payments', post, options) + end + + def capture(authorization) + commit(:post, "payments/#{authorization}/complete", {}, {}) + end + + def void(authorization, options = {}) + post = {} + + post[:reason] = options[:reason] if options[:reason] + + commit(:post, "payments/#{authorization}/cancel", post, {}) + end + + def refund(money, identification, options={}) + post = { payment_id: identification } + + add_idempotency_key(post, options) + add_amount(post, money, options) + + post[:reason] = options[:reason] if options[:reason] + + commit(:post, 'refunds', post, options) + end + + def store(payment, options = {}) + requires!(options, :idempotency_key) + + post = {} + + add_customer(post, options) + add_idempotency_key(post, options) + + MultiResponse.run(:first) do |r| + r.process { commit(:post, 'customers', post, options) } + + r.process { commit(:post, "customers/#{r.params['customer']['id']}/cards", { card_nonce: payment }, options) } if r.success? && r.params && r.params['customer'] && r.params['customer']['id'] + end + end + + def unstore(identification, options = {}) + commit(:delete, "customers/#{identification}", {}, options) + end + + def update_customer(identification, options = {}) + post = {} + add_customer(post, options) + commit(:put, "customers/#{identification}", post, options) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]'). + gsub(/(\\\"source_id\\\":)(\\\".*?")/, '\1[FILTERED]') + end + + private + + def add_idempotency_key(post, options) + post[:idempotency_key] = options[:idempotency_key] unless options.nil? || options[:idempotency_key].nil? || options[:idempotency_key].blank? + end + + def add_amount(post, money, options) + currency = options[:currency] || currency(money) + post[:amount_money] = { + amount: localized_amount(money, currency).to_i, + currency: currency.upcase + } + end + + def add_application_fee(post, money, options) + currency = options[:currency] || currency(money) + if options[:application_fee] + post[:app_fee_money] = { + amount: localized_amount(money, currency).to_i, + currency: currency.upcase + } + end + end + + def create_post_for_auth_or_purchase(money, payment, options) + post = {} + + post[:source_id] = payment + post[:customer_id] = options[:customer] unless options[:customer].nil? || options[:customer].blank? + + add_idempotency_key(post, options) + add_amount(post, money, options) + add_application_fee(post, options[:application_fee], options) + + post + end + + def add_customer(post, options) + first_name = options[:billing_address][:name].split(' ')[0] + last_name = options[:billing_address][:name].split(' ')[1] if options[:billing_address][:name].split(' ').length > 1 + + post[:email_address] = options[:email] || nil + post[:phone_number] = options[:billing_address] ? options[:billing_address][:phone] : nil + post[:given_name] = first_name + post[:family_name] = last_name + + post[:address] = {} + post[:address][:address_line_1] = options[:billing_address] ? options[:billing_address][:address1] : nil + post[:address][:address_line_2] = options[:billing_address] ? options[:billing_address][:address2] : nil + post[:address][:locality] = options[:billing_address] ? options[:billing_address][:city] : nil + post[:address][:administrative_district_level_1] = options[:billing_address] ? options[:billing_address][:state] : nil + post[:address][:administrative_district_level_2] = options[:billing_address] ? options[:billing_address][:country] : nil + post[:address][:country] = options[:billing_address] ? options[:billing_address][:country] : nil + post[:address][:postal_code] = options[:billing_address] ? options[:billing_address][:zip] : nil + end + + def api_request(method, endpoint, parameters = nil, options = {}) + url = (test? ? test_url : live_url) + raw_response = response = nil + begin + raw_response = ssl_request(method, "#{url}/#{endpoint}", parameters.to_json, headers(options)) + response = parse(raw_response) + rescue ResponseError => e + raw_response = e.response.body + response = response_error(raw_response) + rescue JSON::ParserError + response = json_error(raw_response) + end + + return response + end + + def commit(method, url, parameters = nil, options = {}) + response = api_request(method, url, parameters, options) + success = success_from(response) + + card = card_from_response(response) + + avs_code = AVS_CODE_TRANSLATOR[card['avs_status']] + cvc_code = CVC_CODE_TRANSLATOR[card['cvv_status']] + + Response.new( + success, + message_from(success, response), + response, + authorization: authorization_from(success, url, method, response), + avs_result: success ? AVSResult.new(code: avs_code) : nil, + cvv_result: success ? CVVResult.new(cvc_code) : nil, + error_code: success ? nil : error_code_from(response), + test: test? + ) + end + + def card_from_response(response) + return {} unless response['payment'] + + response['payment']['card_details'] || {} + end + + def success_from(response) + !response.key?('errors') + end + + def message_from(success, response) + success ? 'Transaction approved' : response['errors'][0]['detail'] + end + + def authorization_from(success, url, method, response) + # errors.detail is a vague string -- returning the actual transaction ID here makes more sense + # return response.fetch('errors', [])[0]['detail'] unless success + + return nil unless success + + if method == :post && (url == 'payments' || url.match(/payments\/.*\/complete/) || url.match(/payments\/.*\/cancel/)) + return response['payment']['id'] + elsif method == :post && url == 'refunds' + return response['refund']['id'] + elsif method == :post && url == 'customers' + return response['customer']['id'] + elsif method == :post && url.match(/customers\/.*\/cards/) + return response['card']['id'] + elsif method == :put && url.match(/customers/) + return response['customer']['id'] + elsif method == :delete && url.match(/customers/) + return {} + else + return nil + end + end + + def error_code_from(response) + return nil unless response['errors'] + + code = response['errors'][0]['code'] + STANDARD_ERROR_CODE_MAPPING[code] || STANDARD_ERROR_CODE[:processing_error] + end + + def api_version(options) + options[:version] || self.class::DEFAULT_API_VERSION + end + + def headers(options = {}) + key = options[:access_token] || @access_token + + { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{key}", + 'Square-Version' => api_version(options), + } + end + + def parse(body) + JSON.parse(body) + end + + def response_error(raw_response) + parse(raw_response) + rescue JSON::ParserError + json_error(raw_response) + end + + def json_error(raw_response) + msg = 'Invalid response received from the Square API. Please visit https://squareup.com/help if you continue to receive this message.' + msg += " (The raw response returned by the API was #{raw_response.inspect})" + + { + 'errors' => [{ 'message' => msg }] + } + end + end + end +end diff --git a/test/fixtures.yml b/test/fixtures.yml index 66606cf5e6d..85a9179dd88 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -1291,6 +1291,10 @@ spreedly_core: password: "Y2i7AjgU03SUjwY4xnOPqzdsv4dMbPDCQzorAk8Bcoy0U8EIVE4innGjuoMQv7MN" gateway_token: "3gLeg4726V5P0HK7cq7QzHsL0a6" +# Working credentials, no need to replace +square: + access_token: EAAAEBfybUCoyfELbbSshWYKna9FYluyA56pcgXDNtSDULMWEah5Ci4S8XcPKBYz + # Working credentials, no need to replace stripe: login: sk_test_3OD4TdKSIOhDOL2146JJcC79 diff --git a/test/remote/gateways/remote_square_test.rb b/test/remote/gateways/remote_square_test.rb new file mode 100644 index 00000000000..f12b3f655b1 --- /dev/null +++ b/test/remote/gateways/remote_square_test.rb @@ -0,0 +1,175 @@ +require 'test_helper' + +class RemoteSquareTest < Test::Unit::TestCase + def setup + @gateway = SquareGateway.new(fixtures(:square)) + + @amount = 200 + @refund_amount = 100 + + @card_nonce = 'cnon:card-nonce-ok' + @declined_card_nonce = 'cnon:card-nonce-declined' + + @options = { + email: 'customer@example.com', + billing_address: address(), + } + end + + def test_successful_authorize + @options[:idempotency_key] = SecureRandom.hex(10) + + assert authorization = @gateway.authorize(@amount, @card_nonce, @options) + + assert_success authorization + assert_not_nil authorization.authorization + assert_equal 'APPROVED', authorization.params['payment']['status'] + assert_equal @amount, authorization.params['payment']['amount_money']['amount'] + assert_equal @gateway.default_currency.downcase, authorization.params['payment']['amount_money']['currency'].downcase + end + + def test_unsuccessful_authorize + @options[:idempotency_key] = SecureRandom.hex(10) + + assert authorization = @gateway.authorize(@amount, @declined_card_nonce, @options) + + assert_failure authorization + assert_equal 'FAILED', authorization.params['payment']['status'] + end + + def test_successful_authorize_then_capture + @options[:idempotency_key] = SecureRandom.hex(10) + + assert authorization = @gateway.authorize(@amount, @card_nonce, @options) + + assert_success authorization + assert_equal 'APPROVED', authorization.params['payment']['status'] + + assert capture = @gateway.capture(authorization.authorization) + + assert_success capture + assert_equal 'COMPLETED', capture.params['payment']['status'] + end + + def test_successful_authorize_then_void + @options[:idempotency_key] = SecureRandom.hex(10) + + assert authorization = @gateway.authorize(@amount, @card_nonce, @options) + + assert_success authorization + assert_equal 'APPROVED', authorization.params['payment']['status'] + + assert void = @gateway.void(authorization.authorization, @options) + + assert_success void + assert_equal 'CANCELED', void.params['payment']['status'] + end + + def test_successful_purchase + @options[:idempotency_key] = SecureRandom.hex(10) + + assert purchase = @gateway.purchase(@amount, @card_nonce, @options) + + assert_success purchase + assert_equal @amount, purchase.params['payment']['amount_money']['amount'] + assert_equal @gateway.default_currency.downcase, purchase.params['payment']['amount_money']['currency'].downcase + assert_equal 'COMPLETED', purchase.params['payment']['status'] + end + + def test_unsuccessful_purchase + @options[:idempotency_key] = SecureRandom.hex(10) + + assert purchase = @gateway.purchase(@amount, @declined_card_nonce, @options) + + assert_failure purchase + assert_equal 'FAILED', purchase.params['payment']['status'] + end + + def test_successful_purchase_then_refund + @options[:idempotency_key] = SecureRandom.hex(10) + + assert purchase = @gateway.purchase(@amount, @card_nonce, @options) + + assert_success purchase + assert_equal 'COMPLETED', purchase.params['payment']['status'] + + sleep 2 + + @options[:idempotency_key] = SecureRandom.hex(10) + assert refund = @gateway.refund(@refund_amount, purchase.authorization, @options) + + assert_success refund + assert_equal @refund_amount, refund.params['refund']['amount_money']['amount'] + assert_equal @gateway.default_currency.downcase, refund.params['refund']['amount_money']['currency'].downcase + assert_equal 'PENDING', refund.params['refund']['status'] + end + + def test_successful_store + @options[:idempotency_key] = SecureRandom.hex(10) + + assert store = @gateway.store(@card_nonce, @options) + + assert_instance_of MultiResponse, store + assert_success store + assert_equal 2, store.responses.size + + customer_response = store.responses[0] + assert_not_nil customer_response.params['customer']['id'] + assert_equal @options[:email], customer_response.params['customer']['email_address'] + assert_equal @options[:billing_address][:name].split(' ')[0], customer_response.params['customer']['given_name'] + + card_response = store.responses[1] + assert_not_nil card_response.params['card']['id'] + + assert store.test? + end + + def test_unsuccessful_store_invalid_card + @options[:idempotency_key] = SecureRandom.hex(10) + + assert store = @gateway.store(@declined_card_nonce, @options) + + assert_failure store + assert_equal 'INVALID_CARD', store.params['errors'][0]['code'] + end + + def test_successful_store_then_unstore + @options[:idempotency_key] = SecureRandom.hex(10) + + assert store = @gateway.store(@card_nonce, @options) + + assert_success store + customer_response = store.responses[0] + + assert unstore = @gateway.unstore(customer_response.params['customer']['id'], @options) + + assert_success unstore + assert_empty unstore.params + end + + def test_successful_store_then_update + @options[:idempotency_key] = SecureRandom.hex(10) + + assert store = @gateway.store(@card_nonce, @options) + customer_response = store.responses[0] + + assert_equal @options[:billing_address][:name].split(' ')[0], customer_response.params['customer']['given_name'] + + @options[:billing_address][:name] = 'Tom Smith' + assert update = @gateway.update_customer(customer_response.params['customer']['id'], @options) + + assert_equal 'Tom', update.params['customer']['given_name'] + assert_equal 'Smith', update.params['customer']['family_name'] + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @options[:idempotency_key] = SecureRandom.hex(10) + @gateway.purchase(@amount, @card_nonce, @options) + end + transcript = @gateway.scrub(transcript) + + assert_scrubbed(@gateway.options[:access_token], transcript) + assert_scrubbed(@card_nonce, transcript) + end +end diff --git a/test/unit/gateways/square_test.rb b/test/unit/gateways/square_test.rb new file mode 100644 index 00000000000..c7004a49131 --- /dev/null +++ b/test/unit/gateways/square_test.rb @@ -0,0 +1,557 @@ +require 'test_helper' + +class SquareTest < Test::Unit::TestCase + def setup + @gateway = SquareGateway.new(access_token: 'token') + + @amount = 200 + @refund_amount = 100 + + @card_nonce = 'cnon:card-nonce-ok' + @declined_card_nonce = 'cnon:card-nonce-declined' + + @options = { + email: 'customer@example.com', + billing_address: address(), + } + end + + def test_successful_authorize + @gateway.expects(:ssl_request).returns(successful_authorize_response) + + assert response = @gateway.authorize(@amount, @card_nonce, @options) + + assert_instance_of Response, response + assert_success response + assert_equal 'iqrBxAil6rmDtr7cak9g9WO8uaB', response.authorization + assert_equal 'APPROVED', response.params['payment']['status'] + assert response.test? + end + + def test_unsuccessful_authorize + @gateway.expects(:ssl_request).returns(unsuccessful_authorize_response) + + assert response = @gateway.authorize(@amount, @card_nonce, @options) + + assert_instance_of Response, response + assert_success response + assert_equal 'iqrBxAil6rmDtr7cak9g9WO8uaB', response.authorization + assert_equal 'FAILED', response.params['payment']['status'] + assert response.test? + end + + def test_successful_capture + @gateway.expects(:ssl_request).returns(successful_purchase_capture_reponse) + + assert response = @gateway.capture('EdMl5lwmBxd3ZvsvinkAT5LtvaB') + + assert_instance_of Response, response + assert_success response + assert_equal 'EdMl5lwmBxd3ZvsvinkAT5LtvaB', response.authorization + assert_equal 'COMPLETED', response.params['payment']['status'] + assert response.test? + end + + def test_successful_void + @gateway.expects(:ssl_request).returns(successful_purchase_void_response) + + assert response = @gateway.void('EdMl5lwmBxd3ZvsvinkAT5LtvaB') + + assert_instance_of Response, response + assert_success response + assert_equal 'EdMl5lwmBxd3ZvsvinkAT5LtvaB', response.authorization + assert_equal 'CANCELED', response.params['payment']['status'] + assert response.test? + end + + def test_successful_purchase + @gateway.expects(:ssl_request).returns(successful_purchase_response) + + assert response = @gateway.purchase(@amount, @card_nonce, @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'iqrBxAil6rmDtr7cak9g9WO8uaB', response.authorization + assert_equal 'COMPLETED', response.params['payment']['status'] + assert response.test? + end + + def test_unsuccessful_purchase + @gateway.expects(:ssl_request).returns(unsuccessful_purchase_response) + + assert response = @gateway.purchase(@amount, @card_nonce, @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'iqrBxAil6rmDtr7cak9g9WO8uaB', response.authorization + assert_equal 'FAILED', response.params['payment']['status'] + assert response.test? + end + + def test_successful_purchase_then_refund + @gateway.expects(:ssl_request).returns(successful_refund_response) + + assert response = @gateway.refund(@refund_amount, 'UNOE3kv2BZwqHlJ830RCt5YCuaB', @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'UNOE3kv2BZwqHlJ830RCt5YCuaB_xVteEWVFkXDvKN1ddidfJWipt8p9whmElKT5mZtJ7wZ', response.authorization + assert_equal 'PENDING', response.params['refund']['status'] + assert_equal @refund_amount, response.params['refund']['amount_money']['amount'] + assert_equal 'Customer Canceled', response.params['refund']['reason'] + assert response.test? + end + + def test_successful_store + @gateway.expects(:ssl_request).twice.returns(successful_new_customer_response, successful_new_card_response) + + @options[:idempotency_key] = SecureRandom.hex(10) + + assert response = @gateway.store(@card_nonce, @options) + + assert_instance_of MultiResponse, response + assert_success response + assert_equal 2, response.responses.size + + customer_response = response.responses[0] + assert_not_nil customer_response.params['customer']['id'] + + card_response = response.responses[1] + assert_not_nil card_response.params['card']['id'] + + assert response.test? + end + + def test_successful_store_then_update + @gateway.expects(:ssl_request).returns(successful_update_response) + + @options[:billing_address][:name] = 'Tom Smith' + assert response = @gateway.update_customer('JDKYHBWT1D4F8MFH63DBMEN8Y4', @options) + assert_instance_of Response, response + assert_success response + + assert_equal 'JDKYHBWT1D4F8MFH63DBMEN8Y4', response.authorization + assert_equal 'Tom', response.params['customer']['given_name'] + assert_equal 'Smith', response.params['customer']['family_name'] + assert response.test? + end + + def test_transcript_scrubbing + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + private + + def pre_scrubbed + "opening connection to connect.squareupsandbox.com:443...\n" \ + "opened\n" \ + "starting SSL for connect.squareupsandbox.com:443...\n" \ + "SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256\n" \ + "<- \"POST /v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer 098123098123098123\\r\\nSquare-Version: 2019-10-23\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nHost: connect.squareupsandbox.com\\r\\nContent-Length: 142\\r\\n\\r\\n\"\n" \ + "<- \"{\\\"source_id\\\":\\\"cnon:card-nonce-ok\\\",\\\"idempotency_key\\\":\\\"af43257576422c182b5c\\\",\\\"amount_money\\\":{\\\"amount\\\":200,\\\"currency\\\":\\\"USD\\\"},\\\"autocomplete\\\":true}\"\n" \ + "-> \"HTTP/1.1 200 OK\\r\\n\"\n" \ + "-> \"Date: Mon, 11 Nov 2019 23:35:36 GMT\\r\\n\"\n" \ + "-> \"Frame-Options: DENY\\r\\n\"\n" \ + "-> \"X-Frame-Options: DENY\\r\\n\"\n" \ + "-> \"X-Content-Type-Options: nosniff\\r\\n\"\n" \ + "-> \"X-Xss-Protection: 1; mode=block\\r\\n\"\n" \ + "-> \"Content-Type: application/json\\r\\n\"\n" \ + "-> \"Square-Version: 2019-10-23\\r\\n\"\n" \ + "-> \"Squareup--Connect--V2--Common--Versionmetadata-Bin: CgoyMDE5LTEwLTIz\\r\\n\"\n" \ + "-> \"Vary: Accept-Encoding, User-Agent\\r\\n\"\n" \ + "-> \"Content-Encoding: gzip\\r\\n\"\n" \ + "-> \"Content-Length: 474\\r\\n\"\n" \ + "-> \"X-Envoy-Upstream-Service-Time: 441\\r\\n\"\n" \ + "-> \"Strict-Transport-Security: max-age=604800; includeSubDomains\\r\\n\"\n" \ + "-> \"Server: envoy\\r\\n\"\n" \ + "-> \"Connection: close\\r\\n\"\n" \ + "-> \"\\r\\n\"\n" \ + "reading 474 bytes...\n" \ + "-> \"\\x1F\\x8B\\b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8DR\\xDB\\x8E\\xDA0\\x10}\\xEFW ?\\xAE\\x96*\\xCE\\r\\x96\\xB7\\x00NUn\\xCB\\xE6\\xB6\\xB0/\\x96\\x93\\x98\\x12\\x1A\\x92`;n\\xB3+\\xFE\\xBDv\\xE8J\\xBC\\xB5R$\\xC7\\xE7\\x9C\\x9993\\x9E\\x0F\\xD0\\x90\\xEEL+\\x01&\\x83\\x0FP\\xE4\\xEA\\x00?\\xD3\\xD4w.\\x87\\xE2\\xC2\\x02W4\\x87e\\xB7\\x90\\xE9z3:\\xB5d\\n\\x1EA\\xC6(\\x114\\xC7D\\x87\\x00\\xD3\\x80OC\\b\\xD5\\x17\\x99\\xD6\\xC4r&\\x96\\xFB\\xD5u\\xE1\\x9B\\x12\\xB6M\\xFE\\x0F\\xE1\\xD8\\x18i!9\\xD7m%\\xF0\\xB9\\xAEh\\xD7\\xDB\\xB8\\x01\\xEA\\xD74\\fU\\xB0e\\x8CV\\x99\\xA6@\\x1C\\xCE\\xC1\\xF5\\x11pAD\\xCB50{^oW(Bs\\x95\\x87\\xD7-\\xCB(\\x16]C{\\xCA\\v4\\x9A\\x11\\x96\\xE3\\x9C\\nR\\x94\\xBC\\xCF~\\x17\\xECm\\xA38@\\x9F\\xAA\\x9E\\xED\\xE5)#U?\\no\\x8D\\x82\\xEF3o\\x83\\xD1n\\e\\xA00T\\xCA\\x92p\\x81mM\\xBA\\x8Ec(\\x80\\xFEn\\xB4wqT\\x18\\x84\\xB7{G\\t\\xEB\\xFD\\x9B\\n8\\x14\\xD5\\x0F\\xCA\\x1AV\\xF4=\\x01~\\x19\\xC2\\xE1;\\x1A\\xA1#]]x\\x11o\\xA4\\xB4\\xDB\\xE3\\x8E\\e\\xBB(x:m=_\\x8E\\xE1\\xCB\\xF8\\xB48\\x99\\x81\\x9D\\x8B\\xD7\\x83\\xBD\\x92\\xBF\\xFC\\xF5\\xF4\\xBD\\xC4\\xE1\\xC8\\xAD\\x17\\\"\\xE7\\x8E/\\x13OUN\\x8BJ'\\xB4F\\xD0t-=\\x17\\xF5\\x8E\\xAC\\xC3g*\\x8Euo\\x7F\\x89\\xF6\\xB7\\xEE\\xA4\\xC4wm'\\t\\xF6f3\\xB4\\xBD\\x8D\\x8DH~GzIxOj\\x82\\xEA\\xFDP#\\xE4\\x19+\\x1AQ\\xD4}\\xD1\\xF0e\\xF00G\\xBE\\x17\\xAF\\xA2A\\x84\\xC2h\\xA0\\x82\\x9E\\xE3M\\xA4m\\x94uF\\xB4\\x0E\\xDF\\xF6\\xC9L\\\"?\\xD9{\\xD6\\xDA0\\x96H\\xE5\\xACYN\\xD9_\\x8E\\xBA\\x94Ve{\\x99\\xDA\\xAF<\\x90\\xB4\\x81\\xD2\\x87\\xDF\\xA6\\xE1f\\x15Fo{\\xA5\\x15\\xB5 \\xE5\\xFF\\xAF\\xC6\\xF5\\xFA\\xE5\\x0F\\xF7\\x80\\xC4p\\xD2\\x02\\x00\\x00\"\n" \ + "read 474 bytes\n" \ + "Conn close\n" + end + + def post_scrubbed + "opening connection to connect.squareupsandbox.com:443...\n" \ + "opened\n" \ + "starting SSL for connect.squareupsandbox.com:443...\n" \ + "SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256\n" \ + "<- \"POST /v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nAuthorization: Bearer [FILTERED]\\r\\nSquare-Version: 2019-10-23\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nHost: connect.squareupsandbox.com\\r\\nContent-Length: 142\\r\\n\\r\\n\"\n" \ + "<- \"{\\\"source_id\\\":[FILTERED],\\\"idempotency_key\\\":\\\"af43257576422c182b5c\\\",\\\"amount_money\\\":{\\\"amount\\\":200,\\\"currency\\\":\\\"USD\\\"},\\\"autocomplete\\\":true}\"\n" \ + "-> \"HTTP/1.1 200 OK\\r\\n\"\n" \ + "-> \"Date: Mon, 11 Nov 2019 23:35:36 GMT\\r\\n\"\n" \ + "-> \"Frame-Options: DENY\\r\\n\"\n" \ + "-> \"X-Frame-Options: DENY\\r\\n\"\n" \ + "-> \"X-Content-Type-Options: nosniff\\r\\n\"\n" \ + "-> \"X-Xss-Protection: 1; mode=block\\r\\n\"\n" \ + "-> \"Content-Type: application/json\\r\\n\"\n" \ + "-> \"Square-Version: 2019-10-23\\r\\n\"\n" \ + "-> \"Squareup--Connect--V2--Common--Versionmetadata-Bin: CgoyMDE5LTEwLTIz\\r\\n\"\n" \ + "-> \"Vary: Accept-Encoding, User-Agent\\r\\n\"\n" \ + "-> \"Content-Encoding: gzip\\r\\n\"\n" \ + "-> \"Content-Length: 474\\r\\n\"\n" \ + "-> \"X-Envoy-Upstream-Service-Time: 441\\r\\n\"\n" \ + "-> \"Strict-Transport-Security: max-age=604800; includeSubDomains\\r\\n\"\n" \ + "-> \"Server: envoy\\r\\n\"\n" \ + "-> \"Connection: close\\r\\n\"\n" \ + "-> \"\\r\\n\"\n" \ + "reading 474 bytes...\n" \ + "-> \"\\x1F\\x8B\\b\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8DR\\xDB\\x8E\\xDA0\\x10}\\xEFW ?\\xAE\\x96*\\xCE\\r\\x96\\xB7\\x00NUn\\xCB\\xE6\\xB6\\xB0/\\x96\\x93\\x98\\x12\\x1A\\x92`;n\\xB3+\\xFE\\xBDv\\xE8J\\xBC\\xB5R$\\xC7\\xE7\\x9C\\x9993\\x9E\\x0F\\xD0\\x90\\xEEL+\\x01&\\x83\\x0FP\\xE4\\xEA\\x00?\\xD3\\xD4w.\\x87\\xE2\\xC2\\x02W4\\x87e\\xB7\\x90\\xE9z3:\\xB5d\\n\\x1EA\\xC6(\\x114\\xC7D\\x87\\x00\\xD3\\x80OC\\b\\xD5\\x17\\x99\\xD6\\xC4r&\\x96\\xFB\\xD5u\\xE1\\x9B\\x12\\xB6M\\xFE\\x0F\\xE1\\xD8\\x18i!9\\xD7m%\\xF0\\xB9\\xAEh\\xD7\\xDB\\xB8\\x01\\xEA\\xD74\\fU\\xB0e\\x8CV\\x99\\xA6@\\x1C\\xCE\\xC1\\xF5\\x11pAD\\xCB50{^oW(Bs\\x95\\x87\\xD7-\\xCB(\\x16]C{\\xCA\\v4\\x9A\\x11\\x96\\xE3\\x9C\\nR\\x94\\xBC\\xCF~\\x17\\xECm\\xA38@\\x9F\\xAA\\x9E\\xED\\xE5)#U?\\no\\x8D\\x82\\xEF3o\\x83\\xD1n\\e\\xA00T\\xCA\\x92p\\x81mM\\xBA\\x8Ec(\\x80\\xFEn\\xB4wqT\\x18\\x84\\xB7{G\\t\\xEB\\xFD\\x9B\\n8\\x14\\xD5\\x0F\\xCA\\x1AV\\xF4=\\x01~\\x19\\xC2\\xE1;\\x1A\\xA1#]]x\\x11o\\xA4\\xB4\\xDB\\xE3\\x8E\\e\\xBB(x:m=_\\x8E\\xE1\\xCB\\xF8\\xB48\\x99\\x81\\x9D\\x8B\\xD7\\x83\\xBD\\x92\\xBF\\xFC\\xF5\\xF4\\xBD\\xC4\\xE1\\xC8\\xAD\\x17\\\"\\xE7\\x8E/\\x13OUN\\x8BJ'\\xB4F\\xD0t-=\\x17\\xF5\\x8E\\xAC\\xC3g*\\x8Euo\\x7F\\x89\\xF6\\xB7\\xEE\\xA4\\xC4wm'\\t\\xF6f3\\xB4\\xBD\\x8D\\x8DH~GzIxOj\\x82\\xEA\\xFDP#\\xE4\\x19+\\x1AQ\\xD4}\\xD1\\xF0e\\xF00G\\xBE\\x17\\xAF\\xA2A\\x84\\xC2h\\xA0\\x82\\x9E\\xE3M\\xA4m\\x94uF\\xB4\\x0E\\xDF\\xF6\\xC9L\\\"?\\xD9{\\xD6\\xDA0\\x96H\\xE5\\xACYN\\xD9_\\x8E\\xBA\\x94Ve{\\x99\\xDA\\xAF<\\x90\\xB4\\x81\\xD2\\x87\\xDF\\xA6\\xE1f\\x15Fo{\\xA5\\x15\\xB5 \\xE5\\xFF\\xAF\\xC6\\xF5\\xFA\\xE5\\x0F\\xF7\\x80\\xC4p\\xD2\\x02\\x00\\x00\"\n" \ + "read 474 bytes\n" \ + "Conn close\n" + end + + def successful_authorize_response + <<-RESPONSE + { + "payment": { + "id": "iqrBxAil6rmDtr7cak9g9WO8uaB", + "created_at": "2019-07-10T13:23:49.154Z", + "updated_at": "2019-07-10T13:23:49.446Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "APPROVED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "nsAyY2" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "qHkNOb03hMgEgoP3gyzFBDY3cg4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def unsuccessful_authorize_response + <<-RESPONSE + { + "payment": { + "id": "iqrBxAil6rmDtr7cak9g9WO8uaB", + "created_at": "2019-07-10T13:23:49.154Z", + "updated_at": "2019-07-10T13:23:49.446Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "FAILED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "nsAyY2" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "qHkNOb03hMgEgoP3gyzFBDY3cg4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def successful_purchase_capture_reponse + <<-RESPONSE + { + "payment": { + "id": "EdMl5lwmBxd3ZvsvinkAT5LtvaB", + "created_at": "2019-07-10T13:39:55.317Z", + "updated_at": "2019-07-10T13:40:05.982Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "COMPLETED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "MhIjEN" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "iJbzEHMhcwydeLbN3Apg5ZAjGi4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def successful_purchase_void_response + <<-RESPONSE + { + "payment": { + "id": "EdMl5lwmBxd3ZvsvinkAT5LtvaB", + "created_at": "2019-07-10T13:39:55.317Z", + "updated_at": "2019-07-10T13:40:05.982Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "CANCELED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "MhIjEN" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "iJbzEHMhcwydeLbN3Apg5ZAjGi4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def successful_purchase_response + <<-RESPONSE + { + "payment": { + "id": "iqrBxAil6rmDtr7cak9g9WO8uaB", + "created_at": "2019-07-10T13:23:49.154Z", + "updated_at": "2019-07-10T13:23:49.446Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "COMPLETED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "nsAyY2" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "qHkNOb03hMgEgoP3gyzFBDY3cg4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def unsuccessful_purchase_response + <<-RESPONSE + { + "payment": { + "id": "iqrBxAil6rmDtr7cak9g9WO8uaB", + "created_at": "2019-07-10T13:23:49.154Z", + "updated_at": "2019-07-10T13:23:49.446Z", + "amount_money": { + "amount": 200, + "currency": "USD" + }, + "app_fee_money": { + "amount": 10, + "currency": "USD" + }, + "total_money": { + "amount": 200, + "currency": "USD" + }, + "status": "FAILED", + "source_type": "CARD", + "card_details": { + "status": "CAPTURED", + "card": { + "card_brand": "VISA", + "last_4": "2796", + "exp_month": 7, + "exp_year": 2026, + "fingerprint": "sq-1-TpmjbNBMFdibiIjpQI5LiRgNUBC7u1689i0TgHjnlyHEWYB7tnn-K4QbW4ttvtaqXw" + }, + "entry_method": "ON_FILE", + "cvv_status": "CVV_ACCEPTED", + "avs_status": "AVS_ACCEPTED", + "auth_result_code": "nsAyY2" + }, + "location_id": "XK3DBG77NJBFX", + "order_id": "qHkNOb03hMgEgoP3gyzFBDY3cg4F", + "reference_id": "123456", + "note": "Brief description", + "customer_id": "VDKXEEKPJN48QDG3BGGFAK05P8" + } + } + RESPONSE + end + + def successful_refund_response + <<-RESPONSE + { + "refund": { + "id": "UNOE3kv2BZwqHlJ830RCt5YCuaB_xVteEWVFkXDvKN1ddidfJWipt8p9whmElKT5mZtJ7wZ", + "reason": "Customer Canceled", + "status": "PENDING", + "amount_money": { + "amount": 100, + "currency": "USD" + }, + "payment_id": "UNOE3kv2BZwqHlJ830RCt5YCuaB", + "created_at": "2018-10-17T20:41:55.520Z", + "updated_at": "2018-10-17T20:41:55.520Z" + } + } + RESPONSE + end + + def successful_new_customer_response + <<-RESPONSE + { + "customer": { + "id": "JDKYHBWT1D4F8MFH63DBMEN8Y4" + } + } + RESPONSE + end + + def successful_new_card_response + <<-RESPONSE + { + "card": { + "id": "icard-card_id", + "card_brand": "VISA", + "last_4": "1111", + "exp_month": 11, + "exp_year": 2018, + "cardholder_name": "Amelia Earhart", + "billing_address": { + "address_line_1": "500 Electric Ave", + "address_line_2": "Suite 600", + "locality": "New York", + "administrative_district_level_1": "NY", + "postal_code": "10003", + "country": "US" + } + } + } + RESPONSE + end + + def successful_update_response + <<-RESPONSE + { + "customer": { + "id": "JDKYHBWT1D4F8MFH63DBMEN8Y4", + "created_at": "2016-03-23T20:21:54.859Z", + "updated_at": "2016-03-25T20:21:55Z", + "given_name": "Tom", + "family_name": "Smith", + "email_address": "New.Amelia.Earhart@example.com", + "address": { + "address_line_1": "500 Electric Ave", + "address_line_2": "Suite 600", + "locality": "New York", + "administrative_district_level_1": "NY", + "postal_code": "10003", + "country": "US" + }, + "reference_id": "YOUR_REFERENCE_ID", + "note": "updated customer note", + "groups": [ + { + "id": "16894e93-96eb-4ced-b24b-f71d42bf084c", + "name": "Aviation Enthusiasts" + } + ] + } + } + RESPONSE + end +end