class ActiveMerchant::Billing::SquareGateway
Constants
- STANDARD_ERROR_CODE_MAPPING
Map to Square’s error codes: docs.connect.squareup.com/api/connect/v2/#handlingerrors
Public Class Methods
The ‘login` key is the client_id (also known as application id)
in the dev portal. Get it after you create a new app: https://connect.squareup.com/apps/
The ‘password` is the access token (personal or OAuth) The `location_id` must be fetched initially
https://docs.connect.squareup.com/articles/processing-payment-rest/
The ‘test` indicates if these credentials are for sandbox or
production (money moving) access
# File lib/active_merchant/billing/gateways/square.rb, line 42 def initialize(options={}) requires!(options, :login, :password, :location_id) @client_id = options[:login].strip @bearer_token = options[:password].strip @location_id = options[:location_id].strip super end
Public Instance Methods
Capture is only used if you did an Authorize, (creating a delayed capture). docs.connect.squareup.com/api/connect/v2/#endpoint-capturetransaction Both ‘money` and `options` are unused. Only a full capture is supported.
# File lib/active_merchant/billing/gateways/square.rb, line 132 def capture(ignored_money, txn_id, ignored_options={}) raise ArgumentError('txn_id required') if txn_id.nil? commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/capture") end
See also store(). Options hash takes the keys as defined here: docs.connect.squareup.com/api/connect/v2/#endpoint-createcustomer
# File lib/active_merchant/billing/gateways/square.rb, line 220 def create_customer(options) required_one_of = [:email, :email_address, :family_name, :given_name, :company_name, :phone_number] if required_one_of.none?{|k| options.key?(k)} raise ArgumentError.new("one of these options keys required:" + " #{required_one_of} but none included.") end MultiResponse.run do |r| post = options.slice(*required_one_of - [:email] + [:phone_number, :reference_id, :note, :nickname]) post[:email_address] = options[:email] if options[:email] post[:note] = options[:description] if options[:description] add_address(post, options, :address) r.process{ commit(:post, 'customers', post) } end end
To create a charge on a card using a card nonce:
purchase(money, card_nonce, { ...create transaction options... })
To create a customer and save a card (via card_nonce) to the customer:
purchase(money, card_nonce, {customer: {...params hash same as in store() method...}, ...}) Note for US and CA, you must have {customer: {billing_address: {zip: 12345}}} which passes AVS to store a card. Note this always creates a new customer, so it may make a duplicate customer if this card was associated to another customer previously.
To use a customer’s card on file:
purchase(money, nil, {customer: {id: 'customer-id', card_id: 'card-id'}})
Note this does not update any fields on the customer.
To use a customer, and link a new card to the customer:
purchase(money, card_nonce, {customer: {id: 'customer-id', billing_address: {zip: 12345}})
Note the zip is required to store the new nonce, and it must pass AVS. Note this does not update any other fields on the customer.
As this may make multiple requests, it returns a MultiResponse.
# File lib/active_merchant/billing/gateways/square.rb, line 70 def purchase(money, card_nonce, options={}) raise ArgumentError('money required') if money.nil? if card_nonce.nil? requires!(options, :customer) requires!(options[:customer], :card_id, :id) end if card_nonce && options[:customer] && options[:customer][:card_id] raise ArgumentError('Cannot call with both card_nonce and' + ' options[:customer][:card_id], choose one.') end post = options.slice(:buyer_email_address, :delay_capture, :note, :reference_id) add_idempotency_key(post, options) add_amount(post, money, options) add_address(post, options) post[:reference_id] = options[:order_id] if options[:order_id] post[:note] = options[:description] if options[:description] MultiResponse.run do |r| if options[:customer] && card_nonce # Since customer was passed in, create customer (if needed) and # store card (always in here). options[:customer][:customer_id] = options[:customer][:id] if options[:customer][:id] # To make store() happy. r.process { store(card_nonce, options[:customer]) } # If we just created a customer. if options[:customer][:id].nil? options[:customer][:id] = r.responses.first.params['customer']['id'] end # We always stored a card, so grab it. options[:customer][:card_id] = r.responses.last.params['card']['id'] # Empty the card_nonce, since we now have the card on file. card_nonce = nil # Invariant: we have a customer and a linked card, and our options # hash is correct. end add_payment(post, card_nonce, options) r.process { commit(:post, "locations/#{@location_id}/transactions", post) } end end
Refund refunds a previously Charged transaction. docs.connect.squareup.com/api/connect/v2/#endpoint-createrefund Options require: ‘tender_id`, and permit `idempotency_key`, `reason`.
# File lib/active_merchant/billing/gateways/square.rb, line 140 def refund(money, txn_id, options={}) raise ArgumentError('txn_id required') if txn_id.nil? raise ArgumentError('money required') if money.nil? requires!(options, :tender_id) post = options.slice(:tender_id, :reason) add_idempotency_key(post, options) add_amount(post, money, options) commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/refund", post) end
# File lib/active_merchant/billing/gateways/square.rb, line 246 def scrub(transcript) transcript. gsub(%r((Authorization: Bearer )[^\r\n]+), '\1[FILTERED]'). # Extra [\\]* for test. We do an extra escape in the regex of [\\]* # b/c the remote_square_test.rb seems to double escape the # backslashes before the quote. This ensures tests pass. gsub(%r((\"card_nonce[\\]*\":[\\]*")[^"]+), '\1[FILTERED]') end
Required in options hash one of: a) :customer_id from the Square CreateCustomer endpoint of customer to link to.
Required in the US and CA: options[:billing_address][:zip] (AVS must pass to link) https://docs.connect.squareup.com/api/connect/v2/#endpoint-createcustomercard
b) :email, :family_name, :given_name, :company_name, :phone_number to create a new customer.
Optional: :cardholder_name, :address (to store on customer) Return values (e.g. the card id) are available on the response.params[‘id’]
# File lib/active_merchant/billing/gateways/square.rb, line 177 def store(card_nonce, options = {}) raise ArgumentError('card_nonce required') if card_nonce.nil? raise ArgumentError.new('card_nonce nil but is a required field.') if card_nonce.nil? MultiResponse.run do |r| if !(options[:customer_id]) r.process { create_customer(options) } options[:customer_id] = r.responses.last.params['customer']['id'] end post = options.slice(:cardholder_name, :billing_address) if post[:billing_address].present? post[:billing_address][:postal_code] = post[:billing_address].delete(:zip) end post[:card_nonce] = card_nonce r.process { commit(:post, "customers/#{CGI.escape(options[:customer_id])}/cards", post) } end end
Scrubbing removes the access token from the header and the card_nonce. Square does not let the merchant ever see PCI data. All payment card data is directly handled on Square’s servers via iframes as described here: docs.connect.squareup.com/articles/adding-payment-form/
# File lib/active_merchant/billing/gateways/square.rb, line 242 def supports_scrubbing? true end
docs.connect.squareup.com/api/connect/v2/#endpoint-deletecustomercard Required options[:id] and ‘card_id’ params.
# File lib/active_merchant/billing/gateways/square.rb, line 210 def unstore(card_id, options = {}, deprecated_options = {}) raise ArgumentError.new('card_id nil but is a required field.') if card_id.nil? requires!(options, :customer) requires!(options[:customer], :id) commit(:delete, "customers/#{CGI.escape(options[:customer][:id])}/cards/#{CGI.escape(card_id)}", nil) end
# File lib/active_merchant/billing/gateways/square.rb, line 195 def update(customer_id, card_id, options = {}) raise Exception.new('Square API does not currently support updating' + ' a given card_id, instead create a new one and delete the old one.') end
docs.connect.squareup.com/api/connect/v2/#endpoint-updatecustomer
# File lib/active_merchant/billing/gateways/square.rb, line 201 def update_customer(customer_id, options = {}) raise ArgumentError.new('customer_id nil but is a required field.') if customer_id.nil? options[:email_address] = options[:email] if options[:email] options[:note] = options[:description] if options[:description] commit(:put, "customers/#{CGI.escape(customer_id)}", options) end
Do an Authorize (Charge with delayed capture) and then Void. Storing a card with a customer will do a verify, however a direct verification only endpoint is not exposed today (Oct ‘16).
# File lib/active_merchant/billing/gateways/square.rb, line 161 def verify(card_nonce, options={}) raise ArgumentError('card_nonce required') if card_nonce.nil? MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, card_nonce, options) } r.process(:ignore_result) { void(r.authorization, options) } end end
Void cancels a delayed capture (not-yet-captured) transaction. docs.connect.squareup.com/api/connect/v2/#endpoint-voidtransaction
# File lib/active_merchant/billing/gateways/square.rb, line 153 def void(txn_id, options={}) raise ArgumentError('txn_id required') if txn_id.nil? commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/void") end
Private Instance Methods
# File lib/active_merchant/billing/gateways/square.rb, line 257 def add_address(post, options, non_billing_addr_key = :shipping_address) if address = options[:billing_address] || options[:address] add_address_for(post, address, :billing_address) end non_billing_addr_key = non_billing_addr_key.to_sym if address = options[non_billing_addr_key] || options[:address] add_address_for(post, address, non_billing_addr_key) end end
# File lib/active_merchant/billing/gateways/square.rb, line 267 def add_address_for(post, address, addr_key) addr_key = addr_key.to_sym post[addr_key] ||= {} # Or-Equals in case they passed in using Square's key format post[addr_key][:address_line_1] = address[:address1] if address[:address1] post[addr_key][:address_line_2] = address[:address2] if address[:address2] post[addr_key][:address_line_3] = address[:address3] if address[:address3] post[addr_key][:locality] = address[:city] if address[:city] post[addr_key][:sublocality] = address[:sublocality] if address[:sublocality] post[addr_key][:sublocality_2] = address[:sublocality_2] if address[:sublocality_2] post[addr_key][:sublocality_3] = address[:sublocality_3] if address[:sublocality_3] post[addr_key][:administrative_district_level_1] = address[:state] if address[:state] post[addr_key][:administrative_district_level_2] = address[:administrative_district_level_2] if address[:administrative_district_level_2] # In the US, this is the county. post[addr_key][:administrative_district_level_3] = address[:administrative_district_level_3] if address[:administrative_district_level_3] # Used in JP not the US post[addr_key][:postal_code] = address[:zip] if address[:zip] post[addr_key][:country] = address[:country] if address[:country] end
# File lib/active_merchant/billing/gateways/square.rb, line 286 def add_amount(post, money, options) post[:amount_money] = {} post[:amount_money][:amount] = Integer(amount(money)) post[:amount_money][:currency] = (options[:currency] || currency(money)) end
# File lib/active_merchant/billing/gateways/square.rb, line 334 def add_idempotency_key(post, options) post[:idempotency_key] = (options[:idempotency_key] || generate_unique_id).to_s end
# File lib/active_merchant/billing/gateways/square.rb, line 293 def add_payment(post, card_nonce, options) if card_nonce.nil? # use card on file requires!(options, :customer) requires!(options[:customer], :id, :card_id) post[:customer_id] = options[:customer][:id] post[:customer_card_id] = options[:customer][:card_id] else # use nonce post[:card_nonce] = card_nonce end end
# File lib/active_merchant/billing/gateways/square.rb, line 354 def api_request(method, endpoint, parameters) json_payload = JSON.generate(parameters) if parameters begin raw_response = ssl_request( method, self.live_url + endpoint, json_payload, headers) response = JSON.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 response end
# File lib/active_merchant/billing/gateways/square.rb, line 306 def commit(method, endpoint, parameters=nil) response = api_request(method, endpoint, parameters) success = !response.key?("errors") Response.new(success, message_from(success, response), response, authorization: authorization_from(response), # Neither avs nor cvv match are not exposed in the api. avs_result: nil, cvv_result: nil, test: test?, error_code: success ? nil : error_code_from(response) ) end
# File lib/active_merchant/billing/gateways/square.rb, line 389 def error_code_from(response) code = response['errors'].first['code'] error_code = STANDARD_ERROR_CODE_MAPPING[code] error_code end
# File lib/active_merchant/billing/gateways/square.rb, line 321 def headers { 'Authorization' => "Bearer " + @bearer_token, 'User-Agent' => "Square/v2 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", 'X-Square-Client-User-Agent' => user_agent, 'Accept' => 'application/json', 'Content-Type' => 'application/json', # Uncomment below to generate request/response json for unit tests. # 'Accept-Encoding' => '' } end
# File lib/active_merchant/billing/gateways/square.rb, line 375 def json_error(raw_response) msg = 'Invalid non-parsable json data response from the Square API.' + ' Please contact' + ' squareup.com/help/us/en/contact?prefill=developer_api' + ' if you continue to receive this message.' + " (The raw API response returned was #{raw_response.inspect})" { "errors" => [{ "category" => "API_ERROR", "detail" => msg }] } end
# File lib/active_merchant/billing/gateways/square.rb, line 339 def message_from(success, response) # e.g. {"errors":[{"category":"INVALID_REQUEST_ERROR","code":"VALUE_TOO_LOW","detail":"`amount_money.amount` must be greater than 100.","field":"amount_money.amount"}]} success ? "Success" : response['errors'].first['detail'] end
# File lib/active_merchant/billing/gateways/square.rb, line 369 def response_error(raw_response) JSON.parse(raw_response) rescue JSON::ParserError json_error(raw_response) end