pay-square 0.1.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +48 -0
- data/MIT-LICENSE +21 -0
- data/README.md +74 -0
- data/Rakefile +15 -0
- data/app/controllers/pay/webhooks/square_controller.rb +48 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20250901000001_add_square_account_to_pay_models.rb +10 -0
- data/lib/pay/square/attributes.rb +39 -0
- data/lib/pay/square/charge.rb +78 -0
- data/lib/pay/square/customer.rb +79 -0
- data/lib/pay/square/engine.rb +30 -0
- data/lib/pay/square/payment_method.rb +37 -0
- data/lib/pay/square/version.rb +5 -0
- data/lib/pay/square/webhooks/card.rb +22 -0
- data/lib/pay/square/webhooks/customer.rb +20 -0
- data/lib/pay/square/webhooks/payment.rb +22 -0
- data/lib/pay/square/webhooks/refund.rb +23 -0
- data/lib/pay/square.rb +129 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 94a7592d85a226f50c70b335728f80342fb8238da4625b0143e3e8ee5fd47ec1
|
|
4
|
+
data.tar.gz: 6d1ee0770171be37469056cb15a23ed5a6c7a699fa864725ff30be5a5ff670b0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 35c828140e98a9469cf68afa423cfa0e00294ddb95c0c6ac8b92e79ec1f9faff5fe4d3d4f0a1e78f20ee669125d4125d65776075e7803efa4281f374c57928e4
|
|
7
|
+
data.tar.gz: e6f863aa1776fe3168a8550927ec83a25ab2188063f5a36389c830d0c3f53f2b03a0975b51fadf386b8467383dff0a866a677db2571e6b038ae268fdc4b2a293
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
* Adopted Pay's `.github` conventions: a `Standardrb` lint job in CI, a reusable `ci.yml`
|
|
6
|
+
(`workflow_call`) that the publish workflow runs before releasing, per-job timeouts, and
|
|
7
|
+
`actions/checkout@v6`. Renamed `release.yml` to `publish_gem.yml`.
|
|
8
|
+
* Added `.standard.yml` (ignoring the generated dummy app), `dependabot.yml` (bundler + GitHub
|
|
9
|
+
Actions, monthly), a pull request template, and bug/feature issue templates.
|
|
10
|
+
|
|
11
|
+
* Added CI (`.github/workflows/ci.yml`): runs the test suite on every pull request and push to
|
|
12
|
+
`main`, across Ruby 3.1–3.4 and Pay 11 + 12, behind a single `All tests passed` check.
|
|
13
|
+
* The release workflow now runs the tests first and only publishes to RubyGems if they pass.
|
|
14
|
+
* `Gemfile` honors `PAY_VERSION` so CI can pin the Pay major under test.
|
|
15
|
+
|
|
16
|
+
* Added trusted-publishing release workflow (`.github/workflows/release.yml`): a `v*` tag push
|
|
17
|
+
publishes to RubyGems via OpenID Connect, no stored API key. See `RELEASING.md`.
|
|
18
|
+
* Removed the generated `gem-push.yml` starter workflow in favor of `release.yml` (it published on
|
|
19
|
+
every push to main, pinned Ruby 2.6, and also pushed to GitHub Packages).
|
|
20
|
+
* README: added gem version / Ruby / license badges and a Links section; vendored the RubyGems
|
|
21
|
+
logos under `assets/rubygems/`.
|
|
22
|
+
* README reorganized to follow Pay's conventions: Pay logo + tagline, badges on one line, a blockquote
|
|
23
|
+
for the upstream endorsement, and a `📚 Docs` section linking to dedicated pages under `docs/`
|
|
24
|
+
(installation, configuration, usage, webhooks).
|
|
25
|
+
* gemspec now sets `rubygems_mfa_required`, and drops the duplicate `homepage_uri` metadata.
|
|
26
|
+
|
|
27
|
+
## 0.1.0.rc1 - 2026-10-21
|
|
28
|
+
|
|
29
|
+
First pre-release, cut for verification in a real consuming app before `0.1.0` is tagged
|
|
30
|
+
(see [RELEASING.md](RELEASING.md) for our pre-release policy).
|
|
31
|
+
|
|
32
|
+
* Compatible with Pay 11.x and 12.x (verified against Pay 11.7.2 and 12.3.0).
|
|
33
|
+
* The Square SDK (`square.rb`) is an app-provided dependency, matching Pay's convention for its
|
|
34
|
+
built-in processors. Add `gem "square.rb", "~> 44"` to your app's Gemfile; `pay-square` no longer
|
|
35
|
+
declares it and no longer `require`s it (the processor gates on `defined?(::Square)`).
|
|
36
|
+
* Added `RELEASING.md` (versioning and tag conventions) and `CONTRIBUTING.md`; documented release
|
|
37
|
+
and pre-release pinning in the README.
|
|
38
|
+
* Fixed test suite boot order so the dummy app's environment loads before the gem.
|
|
39
|
+
* Initial release: Square payment processor for Pay.
|
|
40
|
+
* Charge on a merchant's own connected Square account (per-merchant OAuth) via a
|
|
41
|
+
configurable `Pay::Square.access_token_provider`; a `Square::Client` is built per
|
|
42
|
+
operation and retries once with a refreshed token on a 401.
|
|
43
|
+
* Card-on-file (Cards API), charges (`CreatePayment`) with optional `app_fee_money`,
|
|
44
|
+
and refunds.
|
|
45
|
+
* Pay-mounted `/pay/webhooks/square` endpoint with HMAC signature verification; handlers
|
|
46
|
+
re-fetch authoritative state (Square delivery is unordered).
|
|
47
|
+
* Adds a `square_account` column to scope customers per merchant, with cross-merchant
|
|
48
|
+
isolation in `set/add_payment_processor`.
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Willard Moore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/pay-rails/pay"><img src="https://raw.githubusercontent.com/pay-rails/pay/main/docs/images/logo.svg" height="50px" alt="Pay"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# 💳 Pay::Square — Square for the Pay gem
|
|
6
|
+
|
|
7
|
+
[](https://rubygems.org/gems/pay-square) [](pay-square.gemspec) [](MIT-LICENSE)
|
|
8
|
+
|
|
9
|
+
A **[Square](https://squareup.com/)** payment processor for **[Pay — the payments engine for Ruby on Rails](https://github.com/pay-rails/pay)**.
|
|
10
|
+
|
|
11
|
+
## 🏦 Extending Pay — and why Square is its own gem
|
|
12
|
+
|
|
13
|
+
[Pay](https://github.com/pay-rails/pay) already speaks Stripe, Braintree, Paddle, and Lemon Squeezy.
|
|
14
|
+
`pay-square` adds **Square** from the outside — its own gem, no fork of Pay — the same self-registering
|
|
15
|
+
plugin pattern as [`pay-asaas`](https://github.com/PedroAugustoRamalhoDuarte/pay-asaas).
|
|
16
|
+
|
|
17
|
+
Pay's maintainer, Chris Oliver, both welcomed this and is taking the same direction for Pay's own
|
|
18
|
+
processors:
|
|
19
|
+
|
|
20
|
+
> I'm planning to extract Braintree and Paddle from the main gem. It doesn't bloat anything having them
|
|
21
|
+
> in the gem since they're lazy loaded, but easier to maintain. […] long-term we can add it to the pay
|
|
22
|
+
> org if you would like to keep maintaining it.
|
|
23
|
+
>
|
|
24
|
+
> — [Chris Oliver, pay-rails/pay#1251](https://github.com/pay-rails/pay/pull/1251#issuecomment-5545503069)
|
|
25
|
+
|
|
26
|
+
### Not just a processor — a marketplace integration
|
|
27
|
+
|
|
28
|
+
Braintree and Paddle are being split out for maintainability alone. Square is a different case: here it
|
|
29
|
+
is used for **[marketplace payments](https://github.com/pay-rails/pay/tree/main/docs/marketplaces)** —
|
|
30
|
+
Pay's term for charging on a connected merchant's **own account** and taking an **application fee** —
|
|
31
|
+
not merely to charge your own customers.
|
|
32
|
+
|
|
33
|
+
Square also issues a **distinct OAuth access token per merchant** (unlike Stripe Connect's single
|
|
34
|
+
platform key plus a `stripe_account` header). So `pay-square` charges on a merchant's **own connected
|
|
35
|
+
Square account** (the merchant, not you, is the merchant of record), building a `Square::Client` per
|
|
36
|
+
operation from a token you supply via `Pay::Square.access_token_provider`, and isolates customers per
|
|
37
|
+
merchant with a `square_account` column. That on-behalf-of, per-merchant model — not plain processing —
|
|
38
|
+
is a core reason Square belongs in its own gem.
|
|
39
|
+
|
|
40
|
+
Scope: card payments, card-on-file, refunds, and payment/refund/card/customer webhooks. Square OAuth,
|
|
41
|
+
token refresh, payouts/reconciliation, catalog, and the checkout UI stay in your app.
|
|
42
|
+
|
|
43
|
+
## 📋 Requirements
|
|
44
|
+
|
|
45
|
+
- Ruby >= 3.1.0
|
|
46
|
+
- [Pay](https://github.com/pay-rails/pay) 11.x or 12.x (verified against 11.7.2 and 12.3.0)
|
|
47
|
+
- [square.rb](https://github.com/square/square-ruby-sdk) ~> 44
|
|
48
|
+
|
|
49
|
+
## 📚 Docs
|
|
50
|
+
|
|
51
|
+
- [Installation](docs/installation.md) — Gemfile, pre-releases, and the migration
|
|
52
|
+
- [Configuration](docs/configuration.md) — the per-merchant token resolver and credentials
|
|
53
|
+
- [Usage](docs/usage.md) — charges, card-on-file, refunds, and per-merchant isolation
|
|
54
|
+
- [Webhooks](docs/webhooks.md) — endpoint, signature verification, and handled events
|
|
55
|
+
- [Versioning & releasing](RELEASING.md) — SemVer, tags, pre-releases, and publishing
|
|
56
|
+
- [Contributing](CONTRIBUTING.md) — dev setup, running the tests, and opening issues
|
|
57
|
+
|
|
58
|
+
## 🙏 Contributing
|
|
59
|
+
|
|
60
|
+
Bug reports, feature ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) to
|
|
61
|
+
get started.
|
|
62
|
+
|
|
63
|
+
## 🔗 Links
|
|
64
|
+
|
|
65
|
+
[<img src="assets/rubygems/rubygems_logo_red.png" alt="RubyGems" height="24" align="left">](https://rubygems.org/gems/pay-square)
|
|
66
|
+
|
|
67
|
+
[pay-square on RubyGems](https://rubygems.org/gems/pay-square) · [Changelog](CHANGELOG.md)
|
|
68
|
+
|
|
69
|
+
The RubyGems and Pay logos are trademarks of their respective projects, used here to link to those
|
|
70
|
+
projects, not to imply endorsement.
|
|
71
|
+
|
|
72
|
+
## 📝 License
|
|
73
|
+
|
|
74
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
|
|
4
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
5
|
+
load "rails/tasks/engine.rake"
|
|
6
|
+
|
|
7
|
+
require "rake/testtask"
|
|
8
|
+
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << "test"
|
|
11
|
+
t.pattern = "test/**/*_test.rb"
|
|
12
|
+
t.verbose = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task default: :test
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Webhooks
|
|
3
|
+
class SquareController < ActionController::API
|
|
4
|
+
def create
|
|
5
|
+
if valid_signature?(request.headers["x-square-hmacsha256-signature"])
|
|
6
|
+
queue_event(verify_params.as_json)
|
|
7
|
+
head :ok
|
|
8
|
+
else
|
|
9
|
+
head :bad_request
|
|
10
|
+
end
|
|
11
|
+
rescue Pay::Square::Error
|
|
12
|
+
head :bad_request
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def queue_event(event)
|
|
18
|
+
return unless Pay::Webhooks.delegator.listening?("square.#{params[:type]}")
|
|
19
|
+
|
|
20
|
+
record = Pay::Webhook.create!(processor: :square, event_type: params[:type], event: event)
|
|
21
|
+
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Square signs the HMAC-SHA256 over (notification_url + raw body), base64-encoded.
|
|
25
|
+
# notification_url must match the URL registered with Square; behind a proxy
|
|
26
|
+
# request.original_url may differ, so it is configurable.
|
|
27
|
+
def valid_signature?(signature)
|
|
28
|
+
return false if signature.blank?
|
|
29
|
+
|
|
30
|
+
key = Pay::Square.signing_secret
|
|
31
|
+
return false if key.blank?
|
|
32
|
+
|
|
33
|
+
data = notification_url + request.raw_post
|
|
34
|
+
digest = OpenSSL::Digest.new("sha256")
|
|
35
|
+
hmac = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))
|
|
36
|
+
ActiveSupport::SecurityUtils.secure_compare(hmac, signature)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def notification_url
|
|
40
|
+
Pay::Square.webhook_notification_url.presence || request.original_url
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def verify_params
|
|
44
|
+
params.except(:action, :controller).permit!
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddSquareAccountToPayModels < ActiveRecord::Migration[7.0]
|
|
2
|
+
# Scopes a Pay::Customer / PaymentMethod / Charge to a Square merchant (mirrors
|
|
3
|
+
# stripe_account). Install into your app with:
|
|
4
|
+
# bin/rails pay_square:install:migrations && bin/rails db:migrate
|
|
5
|
+
def change
|
|
6
|
+
add_column :pay_customers, :square_account, :string
|
|
7
|
+
add_column :pay_payment_methods, :square_account, :string
|
|
8
|
+
add_column :pay_charges, :square_account, :string
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
# Pay's core set/add_payment_processor dedup key is hardcoded and does not know about
|
|
4
|
+
# square_account. We prepend to include it for the :square processor so one owner
|
|
5
|
+
# transacting with two Square merchants gets two isolated Pay::Customers (the same
|
|
6
|
+
# guarantee stripe_account provides via #1198). Everything else delegates to super.
|
|
7
|
+
module Attributes
|
|
8
|
+
def set_payment_processor(processor_name, square_account: nil, **options)
|
|
9
|
+
return super(processor_name, **options) unless processor_name.to_sym == :square
|
|
10
|
+
|
|
11
|
+
with_lock do
|
|
12
|
+
pay_customers.update_all(default: false)
|
|
13
|
+
pay_customer = pay_customers.active.where(
|
|
14
|
+
Pay::Customer.inheritance_column => "Pay::Square::Customer",
|
|
15
|
+
:processor => "square",
|
|
16
|
+
:square_account => square_account
|
|
17
|
+
).first_or_initialize
|
|
18
|
+
pay_customer.update!(options.merge(default: true))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
reload_payment_processor
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_payment_processor(processor_name, square_account: nil, **options)
|
|
25
|
+
return super(processor_name, **options) unless processor_name.to_sym == :square
|
|
26
|
+
|
|
27
|
+
pay_customer = pay_customers.active.where(
|
|
28
|
+
Pay::Customer.inheritance_column => "Pay::Square::Customer",
|
|
29
|
+
:processor => "square",
|
|
30
|
+
:square_account => square_account
|
|
31
|
+
).first_or_initialize
|
|
32
|
+
pay_customer.update!(options)
|
|
33
|
+
pay_customer
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Pay::Attributes::CustomerExtension.prepend(Pay::Square::Attributes)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
class Charge < Pay::Charge
|
|
4
|
+
store_accessor :data, :square_order_id
|
|
5
|
+
store_accessor :data, :square_status
|
|
6
|
+
store_accessor :data, :square_merchant_id
|
|
7
|
+
|
|
8
|
+
# Re-fetches authoritative payment state (webhook delivery is unordered), unless a
|
|
9
|
+
# freshly created payment is passed in.
|
|
10
|
+
def self.sync(payment_id, object: nil, pay_customer: nil, try: 0, retries: 1)
|
|
11
|
+
object ||= Pay::Square.with_client(pay_customer) { |client| client.payments.get(payment_id: payment_id) }.payment
|
|
12
|
+
return if object.nil?
|
|
13
|
+
|
|
14
|
+
pay_customer ||= Pay::Customer.find_by(processor: :square, processor_id: object.customer_id)
|
|
15
|
+
if pay_customer.nil?
|
|
16
|
+
Rails.logger.debug "Pay::Customer for Square payment #{payment_id} (customer #{object.customer_id}) is not in the database"
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
card = object.card_details&.card
|
|
21
|
+
attrs = {
|
|
22
|
+
amount: object.amount_money&.amount,
|
|
23
|
+
currency: object.amount_money&.currency&.downcase,
|
|
24
|
+
application_fee_amount: object.app_fee_money&.amount,
|
|
25
|
+
amount_refunded: object.refunded_money&.amount || 0,
|
|
26
|
+
payment_method_type: "card",
|
|
27
|
+
brand: card&.card_brand,
|
|
28
|
+
last4: card&.last_4.to_s,
|
|
29
|
+
exp_month: card&.exp_month&.to_s,
|
|
30
|
+
exp_year: card&.exp_year&.to_s,
|
|
31
|
+
square_account: pay_customer.square_account,
|
|
32
|
+
square_order_id: object.order_id,
|
|
33
|
+
square_status: object.status,
|
|
34
|
+
square_merchant_id: (object.respond_to?(:merchant_id) ? object.merchant_id : nil)
|
|
35
|
+
}
|
|
36
|
+
attrs[:created_at] = Time.zone.parse(object.created_at.to_s) if object.respond_to?(:created_at) && object.created_at.present?
|
|
37
|
+
|
|
38
|
+
if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
|
|
39
|
+
pay_charge.with_lock { pay_charge.update!(attrs) }
|
|
40
|
+
pay_charge
|
|
41
|
+
else
|
|
42
|
+
create!(attrs.merge(customer: pay_customer, processor_id: object.id))
|
|
43
|
+
end
|
|
44
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
45
|
+
if try >= retries
|
|
46
|
+
raise
|
|
47
|
+
else
|
|
48
|
+
sleep 0.15**(try + 1)
|
|
49
|
+
retry_args = {object: object, pay_customer: pay_customer, try: try + 1, retries: retries}
|
|
50
|
+
sync(payment_id, **retry_args)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def api_record
|
|
55
|
+
Pay::Square.with_client(customer) { |client| client.payments.get(payment_id: processor_id) }.payment
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Square refunds are asynchronous (PENDING -> COMPLETED, can fail), so re-sync
|
|
59
|
+
# amount_refunded from the payment rather than optimistically incrementing.
|
|
60
|
+
def refund!(amount_to_refund = nil, **options)
|
|
61
|
+
amount_to_refund ||= amount
|
|
62
|
+
idempotency_key = options.delete(:idempotency_key) || SecureRandom.uuid
|
|
63
|
+
|
|
64
|
+
Pay::Square.with_client(customer) do |client|
|
|
65
|
+
client.refunds.refund_payment(
|
|
66
|
+
idempotency_key: idempotency_key,
|
|
67
|
+
payment_id: processor_id,
|
|
68
|
+
amount_money: {amount: amount_to_refund, currency: (currency || "usd").to_s.upcase}
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
self.class.sync(processor_id, pay_customer: customer)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
ActiveSupport.run_load_hooks :pay_square_charge, Pay::Square::Charge
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
class Customer < Pay::Customer
|
|
4
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::Square::Charge"
|
|
5
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::Square::PaymentMethod"
|
|
6
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::Square::PaymentMethod"
|
|
7
|
+
|
|
8
|
+
# Override this in your Pay::Square::Customer (or via ActiveSupport.on_load
|
|
9
|
+
# :pay_square_customer) to send more fields to Square when creating the customer.
|
|
10
|
+
def api_record_attributes
|
|
11
|
+
{email_address: email, given_name: customer_name}.compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def api_record
|
|
15
|
+
if processor_id?
|
|
16
|
+
Pay::Square.with_client(self) { |client| client.customers.get(customer_id: processor_id) }.customer
|
|
17
|
+
else
|
|
18
|
+
result = Pay::Square.with_client(self) { |client| client.customers.create(**api_record_attributes) }.customer
|
|
19
|
+
update!(processor_id: result.id)
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Pass a stable idempotency_key (e.g. keyed on the order) to make app-level resubmits
|
|
25
|
+
# dedupe; otherwise a generated key covers only this call and its 401 retry.
|
|
26
|
+
def charge(amount, options = {})
|
|
27
|
+
options = options.dup
|
|
28
|
+
idempotency_key = options.delete(:idempotency_key) || SecureRandom.uuid
|
|
29
|
+
currency = (options.delete(:currency) || "usd").to_s.upcase
|
|
30
|
+
source_id = options.delete(:source_id) || options.delete(:payment_method_id) || default_payment_method&.processor_id
|
|
31
|
+
raise Pay::Square::Error, "No source_id or default payment method for Square charge" if source_id.blank?
|
|
32
|
+
|
|
33
|
+
params = {
|
|
34
|
+
idempotency_key: idempotency_key,
|
|
35
|
+
source_id: source_id,
|
|
36
|
+
amount_money: {amount: amount, currency: currency},
|
|
37
|
+
customer_id: processor_id || api_record.id
|
|
38
|
+
}
|
|
39
|
+
if (app_fee = options.delete(:application_fee_amount) || options.delete(:app_fee_amount))
|
|
40
|
+
params[:app_fee_money] = {amount: app_fee, currency: currency}
|
|
41
|
+
end
|
|
42
|
+
params.merge!(options)
|
|
43
|
+
|
|
44
|
+
payment = Pay::Square.with_client(self) { |client| client.payments.create(**params) }.payment
|
|
45
|
+
Pay::Square::Charge.sync(payment.id, object: payment, pay_customer: self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# source_id is a single-use nonce minted client-side by Square's Web Payments SDK.
|
|
49
|
+
def add_payment_method(source_id, default: false)
|
|
50
|
+
api_record unless processor_id?
|
|
51
|
+
idempotency_key = SecureRandom.uuid
|
|
52
|
+
|
|
53
|
+
card = Pay::Square.with_client(self) do |client|
|
|
54
|
+
client.cards.create(idempotency_key: idempotency_key, source_id: source_id, card: {customer_id: processor_id})
|
|
55
|
+
end.card
|
|
56
|
+
|
|
57
|
+
save_payment_method(card, default: default)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update_payment_method(source_id)
|
|
61
|
+
add_payment_method(source_id, default: true)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save_payment_method(card, default:)
|
|
65
|
+
pay_payment_method = payment_methods.where(processor_id: card.id).first_or_initialize
|
|
66
|
+
|
|
67
|
+
attributes = Pay::Square::PaymentMethod.extract_attributes(card).merge(default: default, square_account: square_account)
|
|
68
|
+
|
|
69
|
+
payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
|
|
70
|
+
pay_payment_method.update!(attributes)
|
|
71
|
+
|
|
72
|
+
reload_default_payment_method
|
|
73
|
+
pay_payment_method
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
ActiveSupport.run_load_hooks :pay_square_customer, Pay::Square::Customer
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "rails/engine"
|
|
2
|
+
|
|
3
|
+
module Pay
|
|
4
|
+
module Square
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
engine_name "pay_square"
|
|
7
|
+
|
|
8
|
+
initializer "pay_square.processors" do |app|
|
|
9
|
+
if Pay.automount_routes
|
|
10
|
+
app.routes.append do
|
|
11
|
+
mount Pay::Square::Engine, at: Pay.routes_path
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ActiveSupport.on_load(:active_record) do
|
|
16
|
+
include Pay::Attributes
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Subscribe webhooks before app initializers, matching Pay's own ordering.
|
|
21
|
+
config.before_initialize do
|
|
22
|
+
Pay::Square.configure_webhooks if Pay::Square.enabled?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
config.to_prepare do
|
|
26
|
+
Pay::Square.setup if Pay::Square.enabled?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
class PaymentMethod < Pay::PaymentMethod
|
|
4
|
+
# Pass `object` (the Square card) when you have it; otherwise it is re-fetched.
|
|
5
|
+
def self.sync(card_id, object: nil, pay_customer: nil, try: 0, retries: 1)
|
|
6
|
+
object ||= Pay::Square.with_client(pay_customer) { |client| client.cards.get(card_id: card_id) }.card
|
|
7
|
+
return if object.nil?
|
|
8
|
+
|
|
9
|
+
pay_customer ||= Pay::Customer.find_by(processor: :square, processor_id: object.customer_id)
|
|
10
|
+
return if pay_customer.nil?
|
|
11
|
+
|
|
12
|
+
pay_payment_method = pay_customer.payment_methods.where(processor_id: object.id).first_or_initialize
|
|
13
|
+
pay_payment_method.update!(extract_attributes(object).merge(square_account: pay_customer.square_account))
|
|
14
|
+
pay_payment_method
|
|
15
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
16
|
+
if try >= retries
|
|
17
|
+
raise
|
|
18
|
+
else
|
|
19
|
+
sleep 0.15**(try + 1)
|
|
20
|
+
sync(card_id, object: object, pay_customer: pay_customer, try: try + 1, retries: retries)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.extract_attributes(card)
|
|
25
|
+
{
|
|
26
|
+
payment_method_type: "card",
|
|
27
|
+
brand: card.card_brand,
|
|
28
|
+
last4: card.last_4.to_s,
|
|
29
|
+
exp_month: card.exp_month.to_s,
|
|
30
|
+
exp_year: card.exp_year.to_s
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ActiveSupport.run_load_hooks :pay_square_payment_method, Pay::Square::PaymentMethod
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
module Webhooks
|
|
4
|
+
class Card
|
|
5
|
+
def call(event)
|
|
6
|
+
event = Pay::Square.construct_from_webhook_event(event)
|
|
7
|
+
object = event.data&.object&.card
|
|
8
|
+
return unless object&.id
|
|
9
|
+
|
|
10
|
+
pay_customer = object.customer_id.present? ? Pay::Customer.find_by(processor: :square, processor_id: object.customer_id) : nil
|
|
11
|
+
return unless pay_customer
|
|
12
|
+
|
|
13
|
+
if event.type.to_s == "card.disabled"
|
|
14
|
+
Pay::PaymentMethod.find_by_processor_and_id(:square, object.id)&.destroy
|
|
15
|
+
else
|
|
16
|
+
Pay::Square::PaymentMethod.sync(object.id, pay_customer: pay_customer)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
module Webhooks
|
|
4
|
+
class Customer
|
|
5
|
+
def call(event)
|
|
6
|
+
event = Pay::Square.construct_from_webhook_event(event)
|
|
7
|
+
object = event.data&.object&.customer
|
|
8
|
+
return unless object&.id
|
|
9
|
+
|
|
10
|
+
pay_customer = Pay::Customer.find_by(processor: :square, processor_id: object.id)
|
|
11
|
+
return unless pay_customer
|
|
12
|
+
|
|
13
|
+
if event.type.to_s == "customer.deleted"
|
|
14
|
+
pay_customer.update(deleted_at: Time.current) if pay_customer.respond_to?(:deleted_at)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
module Webhooks
|
|
4
|
+
class Payment
|
|
5
|
+
def call(event)
|
|
6
|
+
event = Pay::Square.construct_from_webhook_event(event)
|
|
7
|
+
object = event.data&.object&.payment
|
|
8
|
+
return unless object&.id
|
|
9
|
+
|
|
10
|
+
pay_customer = object.customer_id.present? ? Pay::Customer.find_by(processor: :square, processor_id: object.customer_id) : nil
|
|
11
|
+
return unless pay_customer
|
|
12
|
+
|
|
13
|
+
pay_charge = Pay::Square::Charge.sync(object.id, pay_customer: pay_customer)
|
|
14
|
+
|
|
15
|
+
if pay_charge && Pay.send_email?(:receipt, pay_charge)
|
|
16
|
+
Pay.mailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).receipt.deliver_later
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Square
|
|
3
|
+
module Webhooks
|
|
4
|
+
class Refund
|
|
5
|
+
def call(event)
|
|
6
|
+
event = Pay::Square.construct_from_webhook_event(event)
|
|
7
|
+
object = event.data&.object&.refund
|
|
8
|
+
return unless object&.payment_id
|
|
9
|
+
|
|
10
|
+
pay_charge = Pay::Charge.find_by(processor_id: object.payment_id)
|
|
11
|
+
pay_customer = pay_charge&.customer || ((object.respond_to?(:customer_id) && object.customer_id.present?) ? Pay::Customer.find_by(processor: :square, processor_id: object.customer_id) : nil)
|
|
12
|
+
return unless pay_customer
|
|
13
|
+
|
|
14
|
+
pay_charge = Pay::Square::Charge.sync(object.payment_id, pay_customer: pay_customer)
|
|
15
|
+
|
|
16
|
+
if pay_charge && Pay.send_email?(:refund, pay_charge)
|
|
17
|
+
Pay.mailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).refund.deliver_later
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/pay/square.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require "pay"
|
|
2
|
+
# square.rb is app-provided (see gemspec); intentionally not required here.
|
|
3
|
+
require "pay/square/version"
|
|
4
|
+
require "pay/square/engine"
|
|
5
|
+
require "pay/square/attributes"
|
|
6
|
+
|
|
7
|
+
module Pay
|
|
8
|
+
module Square
|
|
9
|
+
# Unlike Stripe Connect (one platform key + a stripe_account header), Square issues a
|
|
10
|
+
# distinct OAuth access token per merchant, so the processor builds a Square::Client
|
|
11
|
+
# per operation from a token supplied by access_token_provider.
|
|
12
|
+
class Error < Pay::Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
autoload :Customer, "pay/square/customer"
|
|
16
|
+
autoload :Charge, "pay/square/charge"
|
|
17
|
+
autoload :PaymentMethod, "pay/square/payment_method"
|
|
18
|
+
|
|
19
|
+
module Webhooks
|
|
20
|
+
autoload :Payment, "pay/square/webhooks/payment"
|
|
21
|
+
autoload :Refund, "pay/square/webhooks/refund"
|
|
22
|
+
autoload :Card, "pay/square/webhooks/card"
|
|
23
|
+
autoload :Customer, "pay/square/webhooks/customer"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
extend Pay::Env
|
|
27
|
+
|
|
28
|
+
REQUIRED_VERSION = "~> 44"
|
|
29
|
+
|
|
30
|
+
# A callable accepting (pay_customer, force_refresh = false) that returns a fresh
|
|
31
|
+
# per-merchant OAuth access token. force_refresh is set true on the retry after a 401.
|
|
32
|
+
mattr_accessor :access_token_provider, default: nil
|
|
33
|
+
|
|
34
|
+
def self.enabled?
|
|
35
|
+
return false unless defined?(::Square)
|
|
36
|
+
|
|
37
|
+
# square.rb does not autoload Square::VERSION on `require "square"`.
|
|
38
|
+
current = ::Gem.loaded_specs["square.rb"]&.version&.to_s
|
|
39
|
+
return true if current.nil?
|
|
40
|
+
Pay::Engine.version_matches?(required: REQUIRED_VERSION, current: current) || (raise "[Pay::Square] square.rb gem must be version #{REQUIRED_VERSION}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# No global API key: Square authenticates per-merchant via access_token_provider.
|
|
44
|
+
def self.setup
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.environment
|
|
48
|
+
find_value_by_name(:square, :environment).presence || "production"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.base_url
|
|
52
|
+
(environment.to_s == "sandbox") ? ::Square::Environment::SANDBOX : ::Square::Environment::PRODUCTION
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.signing_secret
|
|
56
|
+
find_value_by_name(:square, :signing_secret)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Square's HMAC covers (notification_url + body), so this must match the URL registered
|
|
60
|
+
# with Square; behind a proxy request.original_url may not.
|
|
61
|
+
def self.webhook_notification_url
|
|
62
|
+
find_value_by_name(:square, :webhook_notification_url)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.access_token(pay_customer, force_refresh: false)
|
|
66
|
+
unless access_token_provider.respond_to?(:call)
|
|
67
|
+
raise Pay::Square::Error, "Pay::Square.access_token_provider is not configured. Set it to a callable that returns a fresh Square OAuth access token for a Pay::Customer."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
token = if access_token_provider.arity == 1 || access_token_provider.arity.zero?
|
|
71
|
+
access_token_provider.call(pay_customer)
|
|
72
|
+
else
|
|
73
|
+
access_token_provider.call(pay_customer, force_refresh)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
raise Pay::Square::Error, "Pay::Square.access_token_provider returned a blank token for pay_customer #{pay_customer&.id}" if token.blank?
|
|
77
|
+
token
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.client(pay_customer, force_refresh: false)
|
|
81
|
+
::Square::Client.new(base_url: base_url, token: access_token(pay_customer, force_refresh: force_refresh))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Retries ONCE with a force-refreshed token on a 401 and maps Square errors to
|
|
85
|
+
# Pay::Square::Error. Callers MUST build any idempotency_key outside this block so the
|
|
86
|
+
# retry reuses the same key and cannot double-charge.
|
|
87
|
+
def self.with_client(pay_customer)
|
|
88
|
+
attempts = 0
|
|
89
|
+
begin
|
|
90
|
+
attempts += 1
|
|
91
|
+
yield client(pay_customer, force_refresh: attempts > 1)
|
|
92
|
+
rescue ::Square::Errors::UnauthorizedError => e
|
|
93
|
+
if attempts < 2
|
|
94
|
+
Rails.logger.info { "[Pay::Square] access token rejected (401) for pay_customer #{pay_customer&.id}; forcing refresh and retrying" }
|
|
95
|
+
retry
|
|
96
|
+
end
|
|
97
|
+
raise Pay::Square::Error, e
|
|
98
|
+
rescue ::Square::Errors::ApiError => e
|
|
99
|
+
raise Pay::Square::Error, e
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.construct_from_webhook_event(event)
|
|
104
|
+
case event
|
|
105
|
+
when Hash
|
|
106
|
+
ActiveSupport::InheritableOptions.new(event.map { |key, value| [key.to_sym, construct_from_webhook_event(value)] }.to_h)
|
|
107
|
+
when Array
|
|
108
|
+
event.map { |value| construct_from_webhook_event(value) }
|
|
109
|
+
else
|
|
110
|
+
event
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.configure_webhooks
|
|
115
|
+
Pay::Webhooks.configure do |events|
|
|
116
|
+
events.subscribe "square.payment.created", Pay::Square::Webhooks::Payment.new
|
|
117
|
+
events.subscribe "square.payment.updated", Pay::Square::Webhooks::Payment.new
|
|
118
|
+
events.subscribe "square.refund.created", Pay::Square::Webhooks::Refund.new
|
|
119
|
+
events.subscribe "square.refund.updated", Pay::Square::Webhooks::Refund.new
|
|
120
|
+
events.subscribe "square.card.created", Pay::Square::Webhooks::Card.new
|
|
121
|
+
events.subscribe "square.card.updated", Pay::Square::Webhooks::Card.new
|
|
122
|
+
events.subscribe "square.card.disabled", Pay::Square::Webhooks::Card.new
|
|
123
|
+
events.subscribe "square.customer.created", Pay::Square::Webhooks::Customer.new
|
|
124
|
+
events.subscribe "square.customer.updated", Pay::Square::Webhooks::Customer.new
|
|
125
|
+
events.subscribe "square.customer.deleted", Pay::Square::Webhooks::Customer.new
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pay-square
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.rc1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Willard Moore
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: pay
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '11.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '13'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '11.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '13'
|
|
32
|
+
description: Adds a Square payment processor to Pay. Charge on a merchant's own connected
|
|
33
|
+
Square account (per-merchant OAuth), with card-on-file, refunds, application fees,
|
|
34
|
+
and webhooks.
|
|
35
|
+
email:
|
|
36
|
+
- willardgmoore@gmail.com
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- CHANGELOG.md
|
|
42
|
+
- MIT-LICENSE
|
|
43
|
+
- README.md
|
|
44
|
+
- Rakefile
|
|
45
|
+
- app/controllers/pay/webhooks/square_controller.rb
|
|
46
|
+
- config/routes.rb
|
|
47
|
+
- db/migrate/20250901000001_add_square_account_to_pay_models.rb
|
|
48
|
+
- lib/pay/square.rb
|
|
49
|
+
- lib/pay/square/attributes.rb
|
|
50
|
+
- lib/pay/square/charge.rb
|
|
51
|
+
- lib/pay/square/customer.rb
|
|
52
|
+
- lib/pay/square/engine.rb
|
|
53
|
+
- lib/pay/square/payment_method.rb
|
|
54
|
+
- lib/pay/square/version.rb
|
|
55
|
+
- lib/pay/square/webhooks/card.rb
|
|
56
|
+
- lib/pay/square/webhooks/customer.rb
|
|
57
|
+
- lib/pay/square/webhooks/payment.rb
|
|
58
|
+
- lib/pay/square/webhooks/refund.rb
|
|
59
|
+
homepage: https://github.com/Willardgmoore/pay-square
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
source_code_uri: https://github.com/Willardgmoore/pay-square
|
|
64
|
+
changelog_uri: https://github.com/Willardgmoore/pay-square/blob/main/CHANGELOG.md
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 3.1.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Square payment processor for the Pay gem
|
|
83
|
+
test_files: []
|