Class: ActiveMerchant::Billing::SquareGateway

Inherits:
Gateway
  • Object
show all
Defined in:
lib/active_merchant/billing/gateways/square.rb

Constant Summary collapse

STANDARD_ERROR_CODE_MAPPING =
{
  'INVALID_CARD' => STANDARD_ERROR_CODE[:invalid_number],
  'INVALID_EXPIRATION' => STANDARD_ERROR_CODE[:invalid_expiry_date],
  'INVALID_EXPIRATION_DATE' => STANDARD_ERROR_CODE[:invalid_expiry_date],
  'INVALID_EXPIRATION_YEAR' => STANDARD_ERROR_CODE[:invalid_expiry_date],

  # Something invalid in the card, e.g. verify declined when linking card to customer.
  'INVALID_CARD_DATA' => STANDARD_ERROR_CODE[:processing_error],
  'CARD_EXPIRED' => STANDARD_ERROR_CODE[:expired_card],
  'VERIFY_CVV_FAILURE' => STANDARD_ERROR_CODE[:incorrect_cvc],
  'VERIFY_AVS_FAILURE' => STANDARD_ERROR_CODE[:incorrect_zip],
  'CARD_DECLINED' => STANDARD_ERROR_CODE[:card_declined],
  'UNAUTHORIZED' => STANDARD_ERROR_CODE[:config_error]
}

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ SquareGateway

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


42
43
44
45
46
47
48
49
# 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

Instance Method Details

#authorize(money, card_nonce, options = {}) ⇒ Object

Authorize for Square uses the Charge with delay_capture = true option. docs.connect.squareup.com/api/connect/v2/#endpoint-charge Same as with ‘purchase`, pass nil for `card_nonce` if using a customer’s stored card on file.

See purchase for more details for calling this.



124
125
126
127
# File 'lib/active_merchant/billing/gateways/square.rb', line 124

def authorize(money, card_nonce, options={})
  options[:delay_capture] = true
  purchase(money, card_nonce, options)
end

#capture(ignored_money, txn_id, ignored_options = {}) ⇒ Object

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.



132
133
134
135
# 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

#create_customer(options) ⇒ Object

See also store(). Options hash takes the keys as defined here: docs.connect.squareup.com/api/connect/v2/#endpoint-createcustomer



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# 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

#purchase(money, card_nonce, options = {}) ⇒ Object

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.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# 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(money, txn_id, options = {}) ⇒ Object

Refund refunds a previously Charged transaction. docs.connect.squareup.com/api/connect/v2/#endpoint-createrefund Options require: ‘tender_id`, and permit `idempotency_key`, `reason`.



140
141
142
143
144
145
146
147
148
149
# 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

#scrub(transcript) ⇒ Object



246
247
248
249
250
251
252
253
# 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

#store(card_nonce, options = {}) ⇒ Object

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’]

Raises:

  • (ArgumentError)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# 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

#supports_scrubbing?Boolean

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/

Returns:

  • (Boolean)


242
243
244
# File 'lib/active_merchant/billing/gateways/square.rb', line 242

def supports_scrubbing?
  true
end

#unstore(card_id, options = {}, deprecated_options = {}) ⇒ Object

Raises:

  • (ArgumentError)


210
211
212
213
214
215
# 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

#update(customer_id, card_id, options = {}) ⇒ Object

Raises:

  • (Exception)


195
196
197
198
# 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

#update_customer(customer_id, options = {}) ⇒ Object

Raises:

  • (ArgumentError)


201
202
203
204
205
206
# 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

#verify(card_nonce, options = {}) ⇒ Object

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).



161
162
163
164
165
166
167
# 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(txn_id, options = {}) ⇒ Object

Void cancels a delayed capture (not-yet-captured) transaction. docs.connect.squareup.com/api/connect/v2/#endpoint-voidtransaction



153
154
155
156
# 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