pushpay-ruby 0.2.0
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 +50 -0
- data/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/lib/pushpay/anticipated_payment.rb +15 -0
- data/lib/pushpay/base.rb +25 -0
- data/lib/pushpay/batch.rb +25 -0
- data/lib/pushpay/client.rb +136 -0
- data/lib/pushpay/configuration.rb +33 -0
- data/lib/pushpay/errors.rb +51 -0
- data/lib/pushpay/fund.rb +40 -0
- data/lib/pushpay/merchant.rb +28 -0
- data/lib/pushpay/organization.rb +30 -0
- data/lib/pushpay/payment.rb +20 -0
- data/lib/pushpay/recurring_payment.rb +25 -0
- data/lib/pushpay/settlement.rb +25 -0
- data/lib/pushpay/version.rb +5 -0
- data/lib/pushpay/webhook.rb +30 -0
- data/lib/pushpay.rb +39 -0
- data/spec/examples.txt +63 -0
- data/spec/pushpay/anticipated_payment_spec.rb +31 -0
- data/spec/pushpay/batch_spec.rb +41 -0
- data/spec/pushpay/client_spec.rb +173 -0
- data/spec/pushpay/configuration_spec.rb +65 -0
- data/spec/pushpay/fund_spec.rb +62 -0
- data/spec/pushpay/merchant_spec.rb +54 -0
- data/spec/pushpay/organization_spec.rb +41 -0
- data/spec/pushpay/payment_spec.rb +58 -0
- data/spec/pushpay/recurring_payment_spec.rb +41 -0
- data/spec/pushpay/settlement_spec.rb +41 -0
- data/spec/pushpay/webhook_spec.rb +63 -0
- data/spec/spec_helper.rb +50 -0
- metadata +191 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a3050bdf6e88d960e882abd161991931364db856d95928ef699febbf990dcc4e
|
|
4
|
+
data.tar.gz: 5a5b91dd42cbbf22f4b9b53b22088d519559b6cbf2c43f764061cb4930c011f8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ee443d65225547f53bb4e2d44add1d7ecf83a6b11d06d1a2bb516cf638297bf67d496f8d3f73c3d6c52a003fe7026ad6c9a0746f9f0cca41bd3001fdbebd07a7
|
|
7
|
+
data.tar.gz: 59a571448c813a1398ba7de36146b0510e7be5f88ea0936c120591de5bb2b4cb7a766cd85070aa2bf90129de8db9fcef64250b7894e1fc922e8b40f0816a366c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Breaking:** Rewritten to match the actual PushPay API
|
|
12
|
+
- Authentication now uses the correct PushPay OAuth 2.0 endpoint (`auth.pushpay.com`) with Basic auth
|
|
13
|
+
- All endpoints use `/v1/` prefix and are scoped by merchant/organization key
|
|
14
|
+
- Configuration uses `client_id`/`client_secret` instead of `application_key`/`application_secret`
|
|
15
|
+
- Responses use HAL+JSON format (`application/hal+json`)
|
|
16
|
+
- Token expiration tracking with automatic re-authentication
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `sandbox!` configuration method for sandbox environment
|
|
20
|
+
- `scopes` configuration for OAuth scope requests
|
|
21
|
+
- `RecurringPayment` resource (list, find, linked payments)
|
|
22
|
+
- `AnticipatedPayment` resource (create, list)
|
|
23
|
+
- `Organization` resource (find, search, in_scope, campuses, merchant_listings)
|
|
24
|
+
- `Fund` resource (CRUD + status updates)
|
|
25
|
+
- `Settlement` resource (list, find, payments)
|
|
26
|
+
- `Batch` resource (list, find, payments)
|
|
27
|
+
- `Webhook` resource (CRUD)
|
|
28
|
+
- `NotFoundError` for 404 responses
|
|
29
|
+
- `RateLimitError` now includes `retry_after` value
|
|
30
|
+
- `Base` resource class with merchant/organization path helpers
|
|
31
|
+
- All resources default to `PushPay.client` when no client is passed
|
|
32
|
+
- `PATCH` support on the HTTP client
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
- `Donation` resource (not a real PushPay API resource)
|
|
36
|
+
- `PaymentPlan` resource (replaced by `RecurringPayment`)
|
|
37
|
+
- `Transaction` resource (not a standalone PushPay API resource)
|
|
38
|
+
- `jwt` dependency (not needed)
|
|
39
|
+
- Hardcoded credentials from client
|
|
40
|
+
- Client-side validation on resources (let the API validate)
|
|
41
|
+
|
|
42
|
+
## [0.1.1] - 2024-12-01
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Initial scaffolding release
|
|
46
|
+
|
|
47
|
+
## [0.1.0] - 2024-11-15
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Eduardo Souza
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# PushPay Ruby
|
|
2
|
+
|
|
3
|
+
Ruby gem for integrating with the [PushPay](https://pushpay.com) payment processing API. Supports payments, recurring payments, anticipated payments, funds, merchants, organizations, settlements, batches, and webhooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'pushpay'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install pushpay
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
PushPay.configure do |config|
|
|
29
|
+
config.client_id = ENV['PUSHPAY_CLIENT_ID']
|
|
30
|
+
config.client_secret = ENV['PUSHPAY_CLIENT_SECRET']
|
|
31
|
+
config.merchant_key = ENV['PUSHPAY_MERCHANT_KEY']
|
|
32
|
+
config.organization_key = ENV['PUSHPAY_ORGANIZATION_KEY']
|
|
33
|
+
|
|
34
|
+
# Optional
|
|
35
|
+
config.scopes = %w[read merchant:view_payments]
|
|
36
|
+
config.timeout = 30 # seconds, default
|
|
37
|
+
|
|
38
|
+
# Use sandbox environment
|
|
39
|
+
# config.sandbox!
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
All resources can be initialized without arguments (uses `PushPay.client` by default) or with an explicit client.
|
|
46
|
+
|
|
47
|
+
### Payments
|
|
48
|
+
|
|
49
|
+
Payments are read-only in the PushPay API.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
payments = PushPay::Payment.new
|
|
53
|
+
|
|
54
|
+
# Get a single payment
|
|
55
|
+
payments.find('payment_token')
|
|
56
|
+
|
|
57
|
+
# List merchant payments with filters
|
|
58
|
+
payments.list(status: 'Success', pageSize: 10, from: '2024-01-01')
|
|
59
|
+
|
|
60
|
+
# List payments across an organization
|
|
61
|
+
payments.list_for_organization
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Recurring Payments
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
recurring = PushPay::RecurringPayment.new
|
|
68
|
+
|
|
69
|
+
# Get a recurring payment
|
|
70
|
+
recurring.find('recurring_token')
|
|
71
|
+
|
|
72
|
+
# List recurring payments
|
|
73
|
+
recurring.list
|
|
74
|
+
|
|
75
|
+
# Get payments linked to a recurring schedule
|
|
76
|
+
recurring.payments('recurring_token')
|
|
77
|
+
|
|
78
|
+
# List across an organization
|
|
79
|
+
recurring.list_for_organization
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Anticipated Payments
|
|
83
|
+
|
|
84
|
+
Create payment links that can be sent to payers.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
anticipated = PushPay::AnticipatedPayment.new
|
|
88
|
+
|
|
89
|
+
# Create an anticipated payment
|
|
90
|
+
anticipated.create({ amount: '50.00' })
|
|
91
|
+
|
|
92
|
+
# List anticipated payments
|
|
93
|
+
anticipated.list
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Merchants
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
merchants = PushPay::Merchant.new
|
|
100
|
+
|
|
101
|
+
# Get a specific merchant
|
|
102
|
+
merchants.find('merchant_key')
|
|
103
|
+
|
|
104
|
+
# Search merchants
|
|
105
|
+
merchants.search(name: 'Church')
|
|
106
|
+
|
|
107
|
+
# List accessible merchants
|
|
108
|
+
merchants.in_scope
|
|
109
|
+
|
|
110
|
+
# Search nearby
|
|
111
|
+
merchants.near(latitude: '37.7749', longitude: '-122.4194', country: 'US')
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Organizations
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
orgs = PushPay::Organization.new
|
|
118
|
+
|
|
119
|
+
# Get an organization
|
|
120
|
+
orgs.find('org_key')
|
|
121
|
+
|
|
122
|
+
# List accessible organizations
|
|
123
|
+
orgs.in_scope
|
|
124
|
+
|
|
125
|
+
# List campuses
|
|
126
|
+
orgs.campuses
|
|
127
|
+
|
|
128
|
+
# List merchant listings
|
|
129
|
+
orgs.merchant_listings
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Funds
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
funds = PushPay::Fund.new
|
|
136
|
+
|
|
137
|
+
# List funds for a merchant
|
|
138
|
+
funds.list
|
|
139
|
+
|
|
140
|
+
# List funds for an organization
|
|
141
|
+
funds.list_for_organization
|
|
142
|
+
|
|
143
|
+
# Get a specific fund
|
|
144
|
+
funds.find('fund_key')
|
|
145
|
+
|
|
146
|
+
# Create a fund
|
|
147
|
+
funds.create({ name: 'Missions', taxDeductible: true })
|
|
148
|
+
|
|
149
|
+
# Update a fund
|
|
150
|
+
funds.update('fund_key', { name: 'Updated Name' })
|
|
151
|
+
|
|
152
|
+
# Delete a fund
|
|
153
|
+
funds.delete('fund_key')
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Settlements
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
settlements = PushPay::Settlement.new
|
|
160
|
+
|
|
161
|
+
# List settlements
|
|
162
|
+
settlements.list
|
|
163
|
+
|
|
164
|
+
# Get a specific settlement
|
|
165
|
+
settlements.find('settlement_key')
|
|
166
|
+
|
|
167
|
+
# Get payments within a settlement
|
|
168
|
+
settlements.payments('settlement_key')
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Batches
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
batches = PushPay::Batch.new
|
|
175
|
+
|
|
176
|
+
# List batches
|
|
177
|
+
batches.list
|
|
178
|
+
|
|
179
|
+
# Get a specific batch
|
|
180
|
+
batches.find('batch_key')
|
|
181
|
+
|
|
182
|
+
# Get payments within a batch
|
|
183
|
+
batches.payments('batch_key')
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Webhooks
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
webhooks = PushPay::Webhook.new
|
|
190
|
+
|
|
191
|
+
# List webhooks
|
|
192
|
+
webhooks.list
|
|
193
|
+
|
|
194
|
+
# Create a webhook
|
|
195
|
+
webhooks.create({ target: 'https://example.com/webhook', eventTypes: ['payment_created'] })
|
|
196
|
+
|
|
197
|
+
# Update a webhook
|
|
198
|
+
webhooks.update('webhook_token', { target: 'https://example.com/new' })
|
|
199
|
+
|
|
200
|
+
# Delete a webhook
|
|
201
|
+
webhooks.delete('webhook_token')
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Using a Custom Merchant/Organization Key
|
|
205
|
+
|
|
206
|
+
All merchant/org-scoped methods accept an optional key override:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
payments.list(merchant_key: 'other_merchant')
|
|
210
|
+
funds.list_for_organization(organization_key: 'other_org')
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Error Handling
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
begin
|
|
217
|
+
payments.list
|
|
218
|
+
rescue PushPay::ConfigurationError => e
|
|
219
|
+
# Missing API credentials
|
|
220
|
+
rescue PushPay::AuthenticationError => e
|
|
221
|
+
# OAuth authentication failed
|
|
222
|
+
rescue PushPay::ValidationError => e
|
|
223
|
+
puts e.errors # Detailed validation failures
|
|
224
|
+
rescue PushPay::NotFoundError => e
|
|
225
|
+
# 404 - Resource not found
|
|
226
|
+
rescue PushPay::RateLimitError => e
|
|
227
|
+
puts e.retry_after # Seconds to wait
|
|
228
|
+
rescue PushPay::APIError => e
|
|
229
|
+
puts e.status_code
|
|
230
|
+
puts e.response_body
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bundle install
|
|
238
|
+
bundle exec rspec
|
|
239
|
+
bundle exec rubocop
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class AnticipatedPayment < Base
|
|
5
|
+
# Create a new anticipated payment
|
|
6
|
+
def create(params, merchant_key: nil)
|
|
7
|
+
client.post("#{merchant_path(merchant_key)}/anticipatedpayments", params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# List anticipated payments for a merchant
|
|
11
|
+
def list(merchant_key: nil, **params)
|
|
12
|
+
client.get("#{merchant_path(merchant_key)}/anticipatedpayments", params)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/pushpay/base.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Base
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(client = nil)
|
|
8
|
+
@client = client || PushPay.client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def merchant_path(merchant_key = nil)
|
|
14
|
+
key = merchant_key || client.configuration.merchant_key
|
|
15
|
+
raise ConfigurationError, ["merchant_key"] unless key
|
|
16
|
+
"/v1/merchant/#{key}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def organization_path(organization_key = nil)
|
|
20
|
+
key = organization_key || client.configuration.organization_key
|
|
21
|
+
raise ConfigurationError, ["organization_key"] unless key
|
|
22
|
+
"/v1/organization/#{key}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Batch < Base
|
|
5
|
+
# Get a specific batch
|
|
6
|
+
def find(batch_key, merchant_key: nil)
|
|
7
|
+
client.get("#{merchant_path(merchant_key)}/batch/#{batch_key}")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# List batches for a merchant
|
|
11
|
+
def list(merchant_key: nil, **params)
|
|
12
|
+
client.get("#{merchant_path(merchant_key)}/batches", params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List batches for an organization
|
|
16
|
+
def list_for_organization(organization_key: nil, **params)
|
|
17
|
+
client.get("#{organization_path(organization_key)}/batches", params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get payments within a batch
|
|
21
|
+
def payments(batch_key, merchant_key: nil, **params)
|
|
22
|
+
client.get("#{merchant_path(merchant_key)}/batch/#{batch_key}/payments", params)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module PushPay
|
|
6
|
+
class Client
|
|
7
|
+
include HTTParty
|
|
8
|
+
|
|
9
|
+
attr_reader :configuration, :access_token, :token_expires_at
|
|
10
|
+
|
|
11
|
+
def initialize(configuration)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
validate_configuration!
|
|
14
|
+
@access_token = nil
|
|
15
|
+
@token_expires_at = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticate!
|
|
19
|
+
credentials = Base64.strict_encode64("#{configuration.client_id}:#{configuration.client_secret}")
|
|
20
|
+
|
|
21
|
+
scopes = configuration.scopes.empty? ? ["read"] : configuration.scopes
|
|
22
|
+
body = { grant_type: 'client_credentials', scope: scopes.join(' ') }
|
|
23
|
+
|
|
24
|
+
response = self.class.post(
|
|
25
|
+
configuration.auth_url,
|
|
26
|
+
body: body,
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization' => "Basic #{credentials}",
|
|
29
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
|
30
|
+
},
|
|
31
|
+
timeout: configuration.timeout
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
unless response.success?
|
|
35
|
+
raise AuthenticationError, "Failed to authenticate: #{response.code} - #{response.body}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@access_token = response['access_token']
|
|
39
|
+
@token_expires_at = Time.now + (response['expires_in'] || 3600)
|
|
40
|
+
@access_token
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get(path, params = {})
|
|
44
|
+
request(:get, path, query: params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def post(path, data = {})
|
|
48
|
+
request(:post, path, body: data.to_json)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def put(path, data = {})
|
|
52
|
+
request(:put, path, body: data.to_json)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def patch(path, data = {})
|
|
56
|
+
request(:patch, path, body: data.to_json)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def delete(path)
|
|
60
|
+
request(:delete, path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def validate_configuration!
|
|
66
|
+
missing = configuration.missing_credentials
|
|
67
|
+
raise ConfigurationError, missing unless missing.empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_authenticated!
|
|
71
|
+
authenticate! if @access_token.nil? || token_expired?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def token_expired?
|
|
75
|
+
@token_expires_at && Time.now >= @token_expires_at
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def request(method, path, options = {})
|
|
79
|
+
ensure_authenticated!
|
|
80
|
+
|
|
81
|
+
url = "#{configuration.base_url}#{path}"
|
|
82
|
+
request_options = {
|
|
83
|
+
headers: auth_headers,
|
|
84
|
+
timeout: configuration.timeout
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if options[:query]
|
|
88
|
+
request_options[:query] = options[:query]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if options[:body]
|
|
92
|
+
request_options[:body] = options[:body]
|
|
93
|
+
request_options[:headers] = request_options[:headers].merge('Content-Type' => 'application/json')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
response = self.class.send(method, url, request_options)
|
|
97
|
+
handle_response(response)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def auth_headers
|
|
101
|
+
{
|
|
102
|
+
'Authorization' => "Bearer #{@access_token}",
|
|
103
|
+
'Accept' => 'application/hal+json'
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_response(response)
|
|
108
|
+
case response.code
|
|
109
|
+
when 200, 201, 202, 204
|
|
110
|
+
response.parsed_response
|
|
111
|
+
when 401
|
|
112
|
+
@access_token = nil
|
|
113
|
+
@token_expires_at = nil
|
|
114
|
+
raise AuthenticationError, "Authentication failed: #{response.body}"
|
|
115
|
+
when 404
|
|
116
|
+
raise NotFoundError.new("Resource not found", response.code, response.body)
|
|
117
|
+
when 429
|
|
118
|
+
retry_after = response.headers['retry-after']
|
|
119
|
+
raise RateLimitError.new("Rate limit exceeded", response.code, response.body, retry_after)
|
|
120
|
+
when 400, 422
|
|
121
|
+
parsed = response.parsed_response || {}
|
|
122
|
+
errors = parsed['validationFailures'] || {}
|
|
123
|
+
raise ValidationError.new(
|
|
124
|
+
parsed['message'] || 'Validation failed',
|
|
125
|
+
response.code,
|
|
126
|
+
response.body,
|
|
127
|
+
errors
|
|
128
|
+
)
|
|
129
|
+
when 500..599
|
|
130
|
+
raise APIError.new("Server error", response.code, response.body)
|
|
131
|
+
else
|
|
132
|
+
raise APIError.new("Unexpected response", response.code, response.body)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :client_id, :client_secret, :merchant_key, :organization_key,
|
|
6
|
+
:base_url, :auth_url, :timeout, :scopes
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@base_url = "https://api.pushpay.io"
|
|
10
|
+
@auth_url = "https://auth.pushpay.com/pushpay/oauth/token"
|
|
11
|
+
@timeout = 30
|
|
12
|
+
@scopes = ["read"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def sandbox!
|
|
16
|
+
@base_url = "https://sandbox-api.pushpay.io"
|
|
17
|
+
@auth_url = "https://auth.pushpay.com/pushpay-sandbox/oauth/token"
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def valid?
|
|
22
|
+
!client_id.nil? && !client_id.empty? &&
|
|
23
|
+
!client_secret.nil? && !client_secret.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def missing_credentials
|
|
27
|
+
missing = []
|
|
28
|
+
missing << "client_id" if client_id.nil? || client_id.to_s.empty?
|
|
29
|
+
missing << "client_secret" if client_secret.nil? || client_secret.to_s.empty?
|
|
30
|
+
missing
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error
|
|
7
|
+
def initialize(missing_credentials)
|
|
8
|
+
super("Missing required credentials: #{missing_credentials.join(', ')}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class AuthenticationError < Error
|
|
13
|
+
def initialize(message = "Authentication failed")
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class APIError < Error
|
|
19
|
+
attr_reader :status_code, :response_body
|
|
20
|
+
|
|
21
|
+
def initialize(message, status_code = nil, response_body = nil)
|
|
22
|
+
super(message)
|
|
23
|
+
@status_code = status_code
|
|
24
|
+
@response_body = response_body
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class ValidationError < APIError
|
|
29
|
+
attr_reader :errors
|
|
30
|
+
|
|
31
|
+
def initialize(message, status_code = nil, response_body = nil, errors = {})
|
|
32
|
+
super(message, status_code, response_body)
|
|
33
|
+
@errors = errors
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NotFoundError < APIError
|
|
38
|
+
def initialize(message = "Resource not found", status_code = 404, response_body = nil)
|
|
39
|
+
super(message, status_code, response_body)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class RateLimitError < APIError
|
|
44
|
+
attr_reader :retry_after
|
|
45
|
+
|
|
46
|
+
def initialize(message = "Rate limit exceeded", status_code = 429, response_body = nil, retry_after = nil)
|
|
47
|
+
super(message, status_code, response_body)
|
|
48
|
+
@retry_after = retry_after
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/pushpay/fund.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Fund < Base
|
|
5
|
+
# Get a specific fund
|
|
6
|
+
def find(fund_key, organization_key: nil)
|
|
7
|
+
client.get("#{organization_path(organization_key)}/fund/#{fund_key}")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# List funds for a merchant
|
|
11
|
+
def list(merchant_key: nil, **params)
|
|
12
|
+
client.get("#{merchant_path(merchant_key)}/funds", params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List funds for an organization
|
|
16
|
+
def list_for_organization(organization_key: nil, **params)
|
|
17
|
+
client.get("#{organization_path(organization_key)}/funds", params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a fund
|
|
21
|
+
def create(params, organization_key: nil)
|
|
22
|
+
client.post("#{organization_path(organization_key)}/funds", params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Update a fund
|
|
26
|
+
def update(fund_key, params, organization_key: nil)
|
|
27
|
+
client.put("#{organization_path(organization_key)}/fund/#{fund_key}", params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Change fund status (open/close)
|
|
31
|
+
def update_status(fund_key, params, organization_key: nil)
|
|
32
|
+
client.patch("#{organization_path(organization_key)}/fund/#{fund_key}", params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Delete a fund
|
|
36
|
+
def delete(fund_key, organization_key: nil)
|
|
37
|
+
client.delete("#{organization_path(organization_key)}/fund/#{fund_key}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PushPay
|
|
4
|
+
class Merchant < Base
|
|
5
|
+
# Get a specific merchant
|
|
6
|
+
def find(merchant_key)
|
|
7
|
+
client.get("/v1/merchant/#{merchant_key}")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Search merchants by name or handle
|
|
11
|
+
def search(**params)
|
|
12
|
+
client.get("/v1/merchants", params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List merchants accessible to the current application
|
|
16
|
+
def in_scope(**params)
|
|
17
|
+
client.get("/v1/merchants/in-scope", params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Search for merchants near a location
|
|
21
|
+
def near(latitude:, longitude:, country: nil, **params)
|
|
22
|
+
query = { latitude: latitude, longitude: longitude }
|
|
23
|
+
query[:country] = country if country
|
|
24
|
+
query.merge!(params)
|
|
25
|
+
client.get("/v1/merchants/near", query)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|