active_merchant_square 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/.travis.yml +9 -0
- data/CONTRIBUTING.txt +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +47 -0
- data/Rakefile +33 -0
- data/active_merchant_square.gemspec +31 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/active_merchant/billing/gateways/square.rb +396 -0
- data/lib/active_merchant_square.rb +7 -0
- data/lib/active_merchant_square/version.rb +3 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f62b79c7b1d1942c4d3b36f1a8b0ec9a1bdd44f
|
4
|
+
data.tar.gz: b68fb0c81d9de4086e26e37703dd91babd361b58
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9115e25a97d496b84fcd9bbd00b15911f8c01c445ec26384fabf1d3350fddef4c4e8c81bfe32ca26f7c454ff6069a1751a850a0c622f3e9915eac46f88ce7b0
|
7
|
+
data.tar.gz: 42d3a01093a27786c58a3887cb867f4c970e0c8ccdf4eb274fc078c3a85c77b64964f03bae007ac4c73f565e316fb9c65fb11aed99f15b3606f552ddf33fd7f8
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/.travis.yml
ADDED
data/CONTRIBUTING.txt
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Contributing
|
2
|
+
============
|
3
|
+
|
4
|
+
If you would like to contribute code to this project you can do so through GitHub by
|
5
|
+
forking the repository and sending a pull request.
|
6
|
+
|
7
|
+
When submitting code, please make every effort to follow existing conventions
|
8
|
+
and style in order to keep the code as readable as possible. Please also make
|
9
|
+
sure your tests passes by running `bundle exec rake test`.
|
10
|
+
|
11
|
+
Before your code can be accepted into the project you must also sign the
|
12
|
+
[Individual Contributor License Agreement (CLA)][1].
|
13
|
+
|
14
|
+
|
15
|
+
[1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017 Square, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Active Merchant Square
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'active_merchant_square'
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install active_merchant_square
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
This simple example demonstrates how a purchase can be made after getting [a card nonce](https://docs.connect.squareup.com/articles/processing-payment-rest#chargingcardnonce)
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'active_merchant_square'
|
25
|
+
|
26
|
+
# Get your login and password by going to: https://connect.squareup.com/apps
|
27
|
+
credentials = {
|
28
|
+
login: 'sandbox-sq0idp-APPLICATION_ID',
|
29
|
+
password: 'sandbox-sq0atb-APPLICATION_SECRET',
|
30
|
+
# How to get your location ID, see: https://docs.connect.squareup.com/articles/faq-lookup-my-location-id
|
31
|
+
location_id: 'LOCATION_ID',
|
32
|
+
}
|
33
|
+
|
34
|
+
amount = 1000 # $10.00
|
35
|
+
|
36
|
+
gateway = ActiveMerchant::Billing::SquareGateway.new(credentials)
|
37
|
+
response = gateway.purchase(amount, card_nonce)
|
38
|
+
|
39
|
+
if response.success?
|
40
|
+
puts "Successfully charged $#{sprintf("%.2f", amount / 100)}"
|
41
|
+
else
|
42
|
+
raise StandardError, response.message
|
43
|
+
end
|
44
|
+
|
45
|
+
```
|
46
|
+
|
47
|
+
For more in-depth documentation and tutorials, see [Square Documentation site](https://docs.connect.squareup.com/)
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'bundler'
|
6
|
+
Bundler.setup
|
7
|
+
rescue LoadError => e
|
8
|
+
puts "Error loading bundler (#{e.message}): \"gem install bundler\" for bundler support."
|
9
|
+
require 'rubygems'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rake'
|
13
|
+
require 'rake/testtask'
|
14
|
+
|
15
|
+
desc "Run the unit test suite"
|
16
|
+
task :default => 'test:units'
|
17
|
+
task :test => 'test:units'
|
18
|
+
|
19
|
+
namespace :test do
|
20
|
+
Rake::TestTask.new(:units) do |t|
|
21
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
22
|
+
t.ruby_opts << '-rubygems -w'
|
23
|
+
t.libs << 'test'
|
24
|
+
t.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
Rake::TestTask.new(:remote) do |t|
|
28
|
+
t.pattern = 'test/remote/**/*_test.rb'
|
29
|
+
t.ruby_opts << '-rubygems -w'
|
30
|
+
t.libs << 'test'
|
31
|
+
t.verbose = true
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_merchant_square/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_merchant_square"
|
8
|
+
spec.version = ActiveMerchantSquare::VERSION
|
9
|
+
spec.authors = ["Francisco Rojas"]
|
10
|
+
spec.email = ["frojas@squareup.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Active Merchant Square is a Square enhancement of the payment abstraction library active_merchant to support for Square's e-commerce API.}
|
13
|
+
spec.description = %q{Use this gem instead of ActiveMerchant gem if you want to use Square's e-commerce APIs (https://squareup.com/developers) with ActiveMerchant. The official ActiveMerchant gem does not support Square because Square shields raw credit card numbers from developers to make PCI compliance easier. This has all ActiveMerchant functionality, plus support of Square's card nonces in the iframe API.}
|
14
|
+
spec.homepage = "https://github.com/square/active_merchant_square"
|
15
|
+
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "test-unit", "~> 3"
|
28
|
+
spec.add_development_dependency 'mocha', '~> 1'
|
29
|
+
|
30
|
+
spec.add_dependency 'activemerchant', "~> 1.66"
|
31
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "active_merchant_square"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,396 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
class SquareGateway < Gateway
|
4
|
+
|
5
|
+
|
6
|
+
# test uses a 'sandbox' access token, same URL
|
7
|
+
self.live_url = 'https://connect.squareup.com/v2/'
|
8
|
+
|
9
|
+
self.money_format = :cents
|
10
|
+
self.supported_countries = ['US', 'CA', 'JP', 'GB', 'AU']
|
11
|
+
self.default_currency = 'USD'
|
12
|
+
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro]
|
13
|
+
|
14
|
+
self.homepage_url = 'https://squareup.com/developers'
|
15
|
+
self.display_name = 'Square'
|
16
|
+
|
17
|
+
# Map to Square's error codes:
|
18
|
+
# https://docs.connect.squareup.com/api/connect/v2/#handlingerrors
|
19
|
+
STANDARD_ERROR_CODE_MAPPING = {
|
20
|
+
'INVALID_CARD' => STANDARD_ERROR_CODE[:invalid_number],
|
21
|
+
'INVALID_EXPIRATION' => STANDARD_ERROR_CODE[:invalid_expiry_date],
|
22
|
+
'INVALID_EXPIRATION_DATE' => STANDARD_ERROR_CODE[:invalid_expiry_date],
|
23
|
+
'INVALID_EXPIRATION_YEAR' => STANDARD_ERROR_CODE[:invalid_expiry_date],
|
24
|
+
|
25
|
+
# Something invalid in the card, e.g. verify declined when linking card to customer.
|
26
|
+
'INVALID_CARD_DATA' => STANDARD_ERROR_CODE[:processing_error],
|
27
|
+
'CARD_EXPIRED' => STANDARD_ERROR_CODE[:expired_card],
|
28
|
+
'VERIFY_CVV_FAILURE' => STANDARD_ERROR_CODE[:incorrect_cvc],
|
29
|
+
'VERIFY_AVS_FAILURE' => STANDARD_ERROR_CODE[:incorrect_zip],
|
30
|
+
'CARD_DECLINED' => STANDARD_ERROR_CODE[:card_declined],
|
31
|
+
'UNAUTHORIZED' => STANDARD_ERROR_CODE[:config_error]
|
32
|
+
}
|
33
|
+
|
34
|
+
# The `login` key is the client_id (also known as application id)
|
35
|
+
# in the dev portal. Get it after you create a new app:
|
36
|
+
# https://connect.squareup.com/apps/
|
37
|
+
# The `password` is the access token (personal or OAuth)
|
38
|
+
# The `location_id` must be fetched initially
|
39
|
+
# https://docs.connect.squareup.com/articles/processing-payment-rest/
|
40
|
+
# The `test` indicates if these credentials are for sandbox or
|
41
|
+
# production (money moving) access
|
42
|
+
def initialize(options={})
|
43
|
+
requires!(options, :login, :password, :location_id)
|
44
|
+
@client_id = options[:login].strip
|
45
|
+
@bearer_token = options[:password].strip
|
46
|
+
@location_id = options[:location_id].strip
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
# To create a charge on a card using a card nonce:
|
52
|
+
# purchase(money, card_nonce, { ...create transaction options... })
|
53
|
+
#
|
54
|
+
# To create a customer and save a card (via card_nonce) to the customer:
|
55
|
+
# purchase(money, card_nonce, {customer: {...params hash same as in store() method...}, ...})
|
56
|
+
# Note for US and CA, you must have {customer: {billing_address: {zip: 12345}}} which passes AVS to store a card.
|
57
|
+
# Note this always creates a new customer, so it may make a duplicate
|
58
|
+
# customer if this card was associated to another customer previously.
|
59
|
+
#
|
60
|
+
# To use a customer's card on file:
|
61
|
+
# purchase(money, nil, {customer: {id: 'customer-id', card_id: 'card-id'}})
|
62
|
+
# Note this does not update any fields on the customer.
|
63
|
+
#
|
64
|
+
# To use a customer, and link a new card to the customer:
|
65
|
+
# purchase(money, card_nonce, {customer: {id: 'customer-id', billing_address: {zip: 12345}})
|
66
|
+
# Note the zip is required to store the new nonce, and it must pass AVS.
|
67
|
+
# Note this does not update any other fields on the customer.
|
68
|
+
#
|
69
|
+
# As this may make multiple requests, it returns a MultiResponse.
|
70
|
+
def purchase(money, card_nonce, options={})
|
71
|
+
raise ArgumentError('money required') if money.nil?
|
72
|
+
if card_nonce.nil?
|
73
|
+
requires!(options, :customer)
|
74
|
+
requires!(options[:customer], :card_id, :id)
|
75
|
+
end
|
76
|
+
if card_nonce && options[:customer] && options[:customer][:card_id]
|
77
|
+
raise ArgumentError('Cannot call with both card_nonce and' +
|
78
|
+
' options[:customer][:card_id], choose one.')
|
79
|
+
end
|
80
|
+
|
81
|
+
post = options.slice(:buyer_email_address, :delay_capture, :note,
|
82
|
+
:reference_id)
|
83
|
+
add_idempotency_key(post, options)
|
84
|
+
add_amount(post, money, options)
|
85
|
+
add_address(post, options)
|
86
|
+
post[:reference_id] = options[:order_id] if options[:order_id]
|
87
|
+
post[:note] = options[:description] if options[:description]
|
88
|
+
|
89
|
+
MultiResponse.run do |r|
|
90
|
+
if options[:customer] && card_nonce
|
91
|
+
# Since customer was passed in, create customer (if needed) and
|
92
|
+
# store card (always in here).
|
93
|
+
options[:customer][:customer_id] = options[:customer][:id] if options[:customer][:id] # To make store() happy.
|
94
|
+
r.process { store(card_nonce, options[:customer]) }
|
95
|
+
|
96
|
+
# If we just created a customer.
|
97
|
+
if options[:customer][:id].nil?
|
98
|
+
options[:customer][:id] =
|
99
|
+
r.responses.first.params['customer']['id']
|
100
|
+
end
|
101
|
+
|
102
|
+
# We always stored a card, so grab it.
|
103
|
+
options[:customer][:card_id] =
|
104
|
+
r.responses.last.params['card']['id']
|
105
|
+
|
106
|
+
# Empty the card_nonce, since we now have the card on file.
|
107
|
+
card_nonce = nil
|
108
|
+
|
109
|
+
# Invariant: we have a customer and a linked card, and our options
|
110
|
+
# hash is correct.
|
111
|
+
end
|
112
|
+
|
113
|
+
add_payment(post, card_nonce, options)
|
114
|
+
r.process { commit(:post, "locations/#{@location_id}/transactions", post) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Authorize for Square uses the Charge with delay_capture = true option.
|
119
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-charge
|
120
|
+
# Same as with `purchase`, pass nil for `card_nonce` if using a customer's
|
121
|
+
# stored card on file.
|
122
|
+
#
|
123
|
+
# See purchase for more details for calling this.
|
124
|
+
def authorize(money, card_nonce, options={})
|
125
|
+
options[:delay_capture] = true
|
126
|
+
purchase(money, card_nonce, options)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Capture is only used if you did an Authorize, (creating a delayed capture).
|
130
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-capturetransaction
|
131
|
+
# Both `money` and `options` are unused. Only a full capture is supported.
|
132
|
+
def capture(ignored_money, txn_id, ignored_options={})
|
133
|
+
raise ArgumentError('txn_id required') if txn_id.nil?
|
134
|
+
commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/capture")
|
135
|
+
end
|
136
|
+
|
137
|
+
# Refund refunds a previously Charged transaction.
|
138
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-createrefund
|
139
|
+
# Options require: `tender_id`, and permit `idempotency_key`, `reason`.
|
140
|
+
def refund(money, txn_id, options={})
|
141
|
+
raise ArgumentError('txn_id required') if txn_id.nil?
|
142
|
+
raise ArgumentError('money required') if money.nil?
|
143
|
+
requires!(options, :tender_id)
|
144
|
+
|
145
|
+
post = options.slice(:tender_id, :reason)
|
146
|
+
add_idempotency_key(post, options)
|
147
|
+
add_amount(post, money, options)
|
148
|
+
commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/refund", post)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Void cancels a delayed capture (not-yet-captured) transaction.
|
152
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-voidtransaction
|
153
|
+
def void(txn_id, options={})
|
154
|
+
raise ArgumentError('txn_id required') if txn_id.nil?
|
155
|
+
commit(:post, "locations/#{CGI.escape(@location_id)}/transactions/#{CGI.escape(txn_id)}/void")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Do an Authorize (Charge with delayed capture) and then Void.
|
159
|
+
# Storing a card with a customer will do a verify, however a direct
|
160
|
+
# verification only endpoint is not exposed today (Oct '16).
|
161
|
+
def verify(card_nonce, options={})
|
162
|
+
raise ArgumentError('card_nonce required') if card_nonce.nil?
|
163
|
+
MultiResponse.run(:use_first_response) do |r|
|
164
|
+
r.process { authorize(100, card_nonce, options) }
|
165
|
+
r.process(:ignore_result) { void(r.authorization, options) }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Required in options hash one of:
|
170
|
+
# a) :customer_id from the Square CreateCustomer endpoint of customer to link to.
|
171
|
+
# Required in the US and CA: options[:billing_address][:zip] (AVS must pass to link)
|
172
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-createcustomercard
|
173
|
+
# b) :email, :family_name, :given_name, :company_name, :phone_number to create a new customer.
|
174
|
+
#
|
175
|
+
# Optional: :cardholder_name, :address (to store on customer)
|
176
|
+
# Return values (e.g. the card id) are available on the response.params['card']['id']
|
177
|
+
def store(card_nonce, options = {})
|
178
|
+
raise ArgumentError('card_nonce required') if card_nonce.nil?
|
179
|
+
raise ArgumentError.new('card_nonce nil but is a required field.') if card_nonce.nil?
|
180
|
+
|
181
|
+
MultiResponse.run do |r|
|
182
|
+
if !(options[:customer_id])
|
183
|
+
r.process { create_customer(options) }
|
184
|
+
options[:customer_id] = r.responses.last.params['customer']['id']
|
185
|
+
end
|
186
|
+
post = options.slice(:cardholder_name, :billing_address)
|
187
|
+
if post[:billing_address].present?
|
188
|
+
post[:billing_address][:postal_code] = post[:billing_address].delete(:zip)
|
189
|
+
end
|
190
|
+
post[:card_nonce] = card_nonce
|
191
|
+
r.process { commit(:post, "customers/#{CGI.escape(options[:customer_id])}/cards", post) }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def update(customer_id, card_id, options = {})
|
196
|
+
raise Exception.new('Square API does not currently support updating' +
|
197
|
+
' a given card_id, instead create a new one and delete the old one.')
|
198
|
+
end
|
199
|
+
|
200
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-updatecustomer
|
201
|
+
def update_customer(customer_id, options = {})
|
202
|
+
raise ArgumentError.new('customer_id nil but is a required field.') if customer_id.nil?
|
203
|
+
options[:email_address] = options[:email] if options[:email]
|
204
|
+
options[:note] = options[:description] if options[:description]
|
205
|
+
commit(:put, "customers/#{CGI.escape(customer_id)}", options)
|
206
|
+
end
|
207
|
+
|
208
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-deletecustomercard
|
209
|
+
# Required options[:customer][:id] and 'card_id' params.
|
210
|
+
def unstore(card_id, options = {}, deprecated_options = {})
|
211
|
+
raise ArgumentError.new('card_id nil but is a required field.') if card_id.nil?
|
212
|
+
requires!(options, :customer)
|
213
|
+
requires!(options[:customer], :id)
|
214
|
+
commit(:delete, "customers/#{CGI.escape(options[:customer][:id])}/cards/#{CGI.escape(card_id)}", nil)
|
215
|
+
end
|
216
|
+
|
217
|
+
# See also store().
|
218
|
+
# Options hash takes the keys as defined here:
|
219
|
+
# https://docs.connect.squareup.com/api/connect/v2/#endpoint-createcustomer
|
220
|
+
def create_customer(options)
|
221
|
+
required_one_of = [:email, :email_address, :family_name, :given_name,
|
222
|
+
:company_name, :phone_number]
|
223
|
+
if required_one_of.none?{|k| options.key?(k)}
|
224
|
+
raise ArgumentError.new("one of these options keys required:" +
|
225
|
+
" #{required_one_of} but none included.")
|
226
|
+
end
|
227
|
+
|
228
|
+
MultiResponse.run do |r|
|
229
|
+
post = options.slice(*required_one_of - [:email] +
|
230
|
+
[:phone_number, :reference_id, :note, :nickname])
|
231
|
+
post[:email_address] = options[:email] if options[:email]
|
232
|
+
post[:note] = options[:description] if options[:description]
|
233
|
+
add_address(post, options, :address)
|
234
|
+
r.process{ commit(:post, 'customers', post) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Scrubbing removes the access token from the header and the card_nonce.
|
239
|
+
# Square does not let the merchant ever see PCI data. All payment card
|
240
|
+
# data is directly handled on Square's servers via iframes as described
|
241
|
+
# here: https://docs.connect.squareup.com/articles/adding-payment-form/
|
242
|
+
def supports_scrubbing?
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
def scrub(transcript)
|
247
|
+
transcript.
|
248
|
+
gsub(%r((Authorization: Bearer )[^\r\n]+), '\1[FILTERED]').
|
249
|
+
# Extra [\\]* for test. We do an extra escape in the regex of [\\]*
|
250
|
+
# b/c the remote_square_test.rb seems to double escape the
|
251
|
+
# backslashes before the quote. This ensures tests pass.
|
252
|
+
gsub(%r((\"card_nonce[\\]*\":[\\]*")[^"]+), '\1[FILTERED]')
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
def add_address(post, options, non_billing_addr_key = :shipping_address)
|
258
|
+
if address = options[:billing_address] || options[:address]
|
259
|
+
add_address_for(post, address, :billing_address)
|
260
|
+
end
|
261
|
+
non_billing_addr_key = non_billing_addr_key.to_sym
|
262
|
+
if address = options[non_billing_addr_key] || options[:address]
|
263
|
+
add_address_for(post, address, non_billing_addr_key)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def add_address_for(post, address, addr_key)
|
268
|
+
addr_key = addr_key.to_sym
|
269
|
+
post[addr_key] ||= {} # Or-Equals in case they passed in using Square's key format
|
270
|
+
post[addr_key][:address_line_1] = address[:address1] if address[:address1]
|
271
|
+
post[addr_key][:address_line_2] = address[:address2] if address[:address2]
|
272
|
+
post[addr_key][:address_line_3] = address[:address3] if address[:address3]
|
273
|
+
|
274
|
+
post[addr_key][:locality] = address[:city] if address[:city]
|
275
|
+
post[addr_key][:sublocality] = address[:sublocality] if address[:sublocality]
|
276
|
+
post[addr_key][:sublocality_2] = address[:sublocality_2] if address[:sublocality_2]
|
277
|
+
post[addr_key][:sublocality_3] = address[:sublocality_3] if address[:sublocality_3]
|
278
|
+
|
279
|
+
post[addr_key][:administrative_district_level_1] = address[:state] if address[:state]
|
280
|
+
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.
|
281
|
+
post[addr_key][:administrative_district_level_3] = address[:administrative_district_level_3] if address[:administrative_district_level_3] # Used in JP not the US
|
282
|
+
post[addr_key][:postal_code] = address[:zip] if address[:zip]
|
283
|
+
post[addr_key][:country] = address[:country] if address[:country]
|
284
|
+
end
|
285
|
+
|
286
|
+
def add_amount(post, money, options)
|
287
|
+
post[:amount_money] = {}
|
288
|
+
post[:amount_money][:amount] = Integer(amount(money))
|
289
|
+
post[:amount_money][:currency] =
|
290
|
+
(options[:currency] || currency(money))
|
291
|
+
end
|
292
|
+
|
293
|
+
def add_payment(post, card_nonce, options)
|
294
|
+
if card_nonce.nil?
|
295
|
+
# use card on file
|
296
|
+
requires!(options, :customer)
|
297
|
+
requires!(options[:customer], :id, :card_id)
|
298
|
+
post[:customer_id] = options[:customer][:id]
|
299
|
+
post[:customer_card_id] = options[:customer][:card_id]
|
300
|
+
else
|
301
|
+
# use nonce
|
302
|
+
post[:card_nonce] = card_nonce
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def commit(method, endpoint, parameters=nil)
|
307
|
+
response = api_request(method, endpoint, parameters)
|
308
|
+
success = !response.key?("errors")
|
309
|
+
Response.new(success,
|
310
|
+
message_from(success, response),
|
311
|
+
response,
|
312
|
+
authorization: authorization_from(response),
|
313
|
+
# Neither avs nor cvv match are not exposed in the api.
|
314
|
+
avs_result: nil,
|
315
|
+
cvv_result: nil,
|
316
|
+
test: test?,
|
317
|
+
error_code: success ? nil : error_code_from(response)
|
318
|
+
)
|
319
|
+
end
|
320
|
+
|
321
|
+
def headers
|
322
|
+
{
|
323
|
+
'Authorization' => "Bearer " + @bearer_token,
|
324
|
+
'User-Agent' =>
|
325
|
+
"Square/v2 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
|
326
|
+
'X-Square-Client-User-Agent' => user_agent,
|
327
|
+
'Accept' => 'application/json',
|
328
|
+
'Content-Type' => 'application/json',
|
329
|
+
# Uncomment below to generate request/response json for unit tests.
|
330
|
+
# 'Accept-Encoding' => ''
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
def add_idempotency_key(post, options)
|
335
|
+
post[:idempotency_key] =
|
336
|
+
(options[:idempotency_key] || generate_unique_id).to_s
|
337
|
+
end
|
338
|
+
|
339
|
+
def message_from(success, response)
|
340
|
+
# e.g. {"errors":[{"category":"INVALID_REQUEST_ERROR","code":"VALUE_TOO_LOW","detail":"`amount_money.amount` must be greater than 100.","field":"amount_money.amount"}]}
|
341
|
+
success ? "Success" : response['errors'].first['detail']
|
342
|
+
end
|
343
|
+
|
344
|
+
def authorization_from(response)
|
345
|
+
if response['transaction']
|
346
|
+
# This will return the transaction level identifier, of which there
|
347
|
+
# is >= 1 nested tender id which you may need to look up depending
|
348
|
+
# on your use case (e.g. refunding). That is available in the
|
349
|
+
# response.transaction.tenders array.
|
350
|
+
return response['transaction']['id']
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def api_request(method, endpoint, parameters)
|
355
|
+
json_payload = JSON.generate(parameters) if parameters
|
356
|
+
begin
|
357
|
+
raw_response = ssl_request(
|
358
|
+
method, self.live_url + endpoint, json_payload, headers)
|
359
|
+
response = JSON.parse(raw_response)
|
360
|
+
rescue ResponseError => e
|
361
|
+
raw_response = e.response.body
|
362
|
+
response = response_error(raw_response)
|
363
|
+
rescue JSON::ParserError
|
364
|
+
response = json_error(raw_response)
|
365
|
+
end
|
366
|
+
response
|
367
|
+
end
|
368
|
+
|
369
|
+
def response_error(raw_response)
|
370
|
+
JSON.parse(raw_response)
|
371
|
+
rescue JSON::ParserError
|
372
|
+
json_error(raw_response)
|
373
|
+
end
|
374
|
+
|
375
|
+
def json_error(raw_response)
|
376
|
+
msg = 'Invalid non-parsable json data response from the Square API.' +
|
377
|
+
' Please contact' +
|
378
|
+
' squareup.com/help/us/en/contact?prefill=developer_api' +
|
379
|
+
' if you continue to receive this message.' +
|
380
|
+
" (The raw API response returned was #{raw_response.inspect})"
|
381
|
+
{
|
382
|
+
"errors" => [{
|
383
|
+
"category" => "API_ERROR",
|
384
|
+
"detail" => msg
|
385
|
+
}]
|
386
|
+
}
|
387
|
+
end
|
388
|
+
|
389
|
+
def error_code_from(response)
|
390
|
+
code = response['errors'].first['code']
|
391
|
+
error_code = STANDARD_ERROR_CODE_MAPPING[code]
|
392
|
+
error_code
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_merchant_square
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Francisco Rojas
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activemerchant
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.66'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.66'
|
83
|
+
description: Use this gem instead of ActiveMerchant gem if you want to use Square's
|
84
|
+
e-commerce APIs (https://squareup.com/developers) with ActiveMerchant. The official
|
85
|
+
ActiveMerchant gem does not support Square because Square shields raw credit card
|
86
|
+
numbers from developers to make PCI compliance easier. This has all ActiveMerchant
|
87
|
+
functionality, plus support of Square's card nonces in the iframe API.
|
88
|
+
email:
|
89
|
+
- frojas@squareup.com
|
90
|
+
executables: []
|
91
|
+
extensions: []
|
92
|
+
extra_rdoc_files: []
|
93
|
+
files:
|
94
|
+
- ".gitignore"
|
95
|
+
- ".travis.yml"
|
96
|
+
- CONTRIBUTING.txt
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- active_merchant_square.gemspec
|
102
|
+
- bin/console
|
103
|
+
- bin/setup
|
104
|
+
- lib/active_merchant/billing/gateways/square.rb
|
105
|
+
- lib/active_merchant_square.rb
|
106
|
+
- lib/active_merchant_square/version.rb
|
107
|
+
homepage: https://github.com/square/active_merchant_square
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.6.11
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Active Merchant Square is a Square enhancement of the payment abstraction
|
131
|
+
library active_merchant to support for Square's e-commerce API.
|
132
|
+
test_files: []
|