wajub 1.1.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/LICENSE +21 -0
- data/README.md +228 -0
- data/lib/wajub/client.rb +48 -0
- data/lib/wajub/crypto.rb +23 -0
- data/lib/wajub/errors.rb +64 -0
- data/lib/wajub/http/base_client.rb +173 -0
- data/lib/wajub/http/http_utils.rb +49 -0
- data/lib/wajub/http/retry_policy.rb +31 -0
- data/lib/wajub/objects.rb +48 -0
- data/lib/wajub/pagination.rb +51 -0
- data/lib/wajub/request_options.rb +13 -0
- data/lib/wajub/resources.rb +282 -0
- data/lib/wajub/version.rb +6 -0
- data/lib/wajub/webhooks.rb +44 -0
- data/lib/wajub.rb +11 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d9c7d3e31696d7f37926d39cba029ab759220d634b654cf3612ca3ae1218932a
|
|
4
|
+
data.tar.gz: ed5abaf317be01db7425470f5a9029b20ed3cf82b026f61f29346296f40ee595
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e060e51ecc44fcf979e9b25f3290ae50101270a046455bf05d63a451abf155f8550fd1ec41f37ea9151372dc548891c67c73bf090041f444493d9066cb4737d2
|
|
7
|
+
data.tar.gz: 89ee4872f3fac3a97ebbc7484f394487bc434f79aeca1fb320348c9424e6d4a097373cdde0a62afb839290d22f14146bd1e1a30faa5a69cf6780591b5e075a64
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wajub
|
|
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,228 @@
|
|
|
1
|
+
# Wajub Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/wajub)
|
|
4
|
+
[](https://www.ruby-lang.org/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Official **server-side** SDK for the [Wajub merchant API](https://docs.wajub.com). Accept mobile-money and card payments across Africa with a Stripe-inspired, resource-oriented client.
|
|
8
|
+
|
|
9
|
+
Use **[Wajub.js](https://docs.wajub.com/libraries/components/js)** for embedded checkout in the browser. Use this SDK on your backend with a secret (`sk_`) or restricted (`rk_`) API key — never expose secret keys in client-side code.
|
|
10
|
+
|
|
11
|
+
This SDK covers the **merchant API**. It does not wrap checkout session endpoints (`/pay/*`), public link checkout (`/q/*`, `/i/*`), or sandbox simulation — those belong to Wajub.js or direct HTTP during the payment flow.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Resource-oriented API (`client.payments`, `client.customers`, …)
|
|
16
|
+
- Automatic `Idempotency-Key` on mutating requests (override per call)
|
|
17
|
+
- Typed errors per HTTP status (`AuthenticationError`, `RateLimitError`, …)
|
|
18
|
+
- Automatic retries on 429 and 5xx (max 2, exponential backoff)
|
|
19
|
+
- Page-based pagination with `auto_paging_each` and `next_page`
|
|
20
|
+
- Webhook signature verification (HMAC-SHA256, timestamp tolerance)
|
|
21
|
+
- Zero third-party runtime dependencies (stdlib only)
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
| Requirement | Version |
|
|
26
|
+
|-------------|---------|
|
|
27
|
+
| Ruby | 3.1 or later |
|
|
28
|
+
| Dependencies | Standard library only (`net/http`, `openssl`, `json`) |
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Add to your Gemfile:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'wajub', '~> 1.1'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install directly:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
gem install wajub
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
Amounts are passed in the **smallest currency unit** (e.g. cents for EUR/USD; whole francs for XAF).
|
|
47
|
+
|
|
48
|
+
### Redirect checkout
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require 'wajub'
|
|
52
|
+
|
|
53
|
+
client = Wajub::Client.new(api_key: ENV['WAJUB_API_KEY'])
|
|
54
|
+
|
|
55
|
+
payment = client.payments.create(
|
|
56
|
+
'amount' => 15_000,
|
|
57
|
+
'currency' => 'XAF',
|
|
58
|
+
'email' => 'buyer@example.com',
|
|
59
|
+
'callback' => 'https://shop.example.com/order/complete'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
puts payment.authorization_url
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Inline / overlay (embed token)
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
embed = client.payments.create(
|
|
69
|
+
'amount' => 15_000,
|
|
70
|
+
'currency' => 'XAF',
|
|
71
|
+
'metadata' => { 'mode' => 'embed' }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Pass to Wajub.js: embed.authorization_token
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`create()` and `retrieve()` return a typed `Payment` object — prefer method access (`payment.authorization_url`). List pages from `list()` yield plain hashes; bracket syntax (`payment['…']`) remains available on typed objects.
|
|
78
|
+
|
|
79
|
+
## Rails
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# config/initializers/wajub.rb
|
|
83
|
+
WajubClient = Wajub::Client.new(
|
|
84
|
+
api_key: Rails.application.credentials.dig(:wajub, :api_key) || ENV['WAJUB_API_KEY'],
|
|
85
|
+
webhook_secret: ENV['WAJUB_WEBHOOK_SECRET']
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Webhook controller
|
|
90
|
+
|
|
91
|
+
Use the **raw request body**:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class WebhooksController < ApplicationController
|
|
95
|
+
skip_before_action :verify_authenticity_token
|
|
96
|
+
|
|
97
|
+
def wajub
|
|
98
|
+
event = WajubClient.webhooks.construct_event(
|
|
99
|
+
request.body.read, # raw body — not params
|
|
100
|
+
request.headers['X-Wajub-Signature'],
|
|
101
|
+
request.headers['X-Wajub-Timestamp']
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
case event['type']
|
|
105
|
+
when 'payment.succeeded'
|
|
106
|
+
# fulfill order
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
head :ok
|
|
110
|
+
rescue Wajub::WebhookSignatureVerificationError
|
|
111
|
+
head :bad_request
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Variable | Description |
|
|
119
|
+
|----------|-------------|
|
|
120
|
+
| `WAJUB_API_KEY` | Secret or restricted API key (`sk_`, `sk_test.`, `rk_`, …) |
|
|
121
|
+
| `WAJUB_WEBHOOK_SECRET` | Webhook signing secret (`whsec_`) for `construct_event()` |
|
|
122
|
+
|
|
123
|
+
Test mode is selected by your API key prefix (`sk_test.…`), not by the API URL. Production calls always go to `https://api.wajub.com`.
|
|
124
|
+
|
|
125
|
+
## Resources (merchant API)
|
|
126
|
+
|
|
127
|
+
| Property | Methods |
|
|
128
|
+
|----------|---------|
|
|
129
|
+
| `client.global` | `ping`, `channels`, `countries`, `currencies` |
|
|
130
|
+
| `client.payments` | `create`, `initialize_payment`, `retrieve`, `list`, `cancel`, `process`, `process_split`, `list_refunds` |
|
|
131
|
+
| `client.customers` | `create`, `retrieve`, `update`, `delete`, `list`, `block`, `unblock`, `activate`, `deactivate`, `list_tax_ids`, `create_tax_id`, `delete_tax_id` |
|
|
132
|
+
| `client.refunds` | `create`, `retrieve`, `list` |
|
|
133
|
+
| `client.transfers` | `create`, `retrieve`, `list` |
|
|
134
|
+
| `client.beneficiaries` | `create`, `retrieve`, `update`, `delete`, `list` |
|
|
135
|
+
| `client.links` | `create`, `retrieve`, `update`, `delete`, `list` |
|
|
136
|
+
| `client.invoices` | `create`, `retrieve`, `update`, `delete`, `list`, `send`, `mark_paid`, `cancel` |
|
|
137
|
+
| `client.accounts` | `create`, `retrieve`, `update`, `delete`, `list`, `regenerate_token` |
|
|
138
|
+
| `client.webhook_endpoints` | `create`, `retrieve`, `update`, `delete`, `list`, `rotate_secret` |
|
|
139
|
+
| `client.balance` | `retrieve` |
|
|
140
|
+
| `client.events` | `list`, `retrieve`, `resend` |
|
|
141
|
+
| `client.disputes` | `list`, `retrieve`, `submit_evidence`, `accept`, `close`, `send_message` |
|
|
142
|
+
| `client.identity` | `resolve`, `validate` |
|
|
143
|
+
| `client.tax` | `get_settings`, `update_settings`, `rates`, `calculate`, `reports`, `list_codes`, `retrieve_code`, `list_registrations`, `create_registration`, `retrieve_registration`, `update_registration`, `delete_registration`, `jurisdictions`, `thresholds`, `threshold_alerts` |
|
|
144
|
+
| `client.shield` | `get_settings`, `update_settings`, `stats`, `list_blocklist`, `add_to_blocklist`, `remove_from_blocklist` |
|
|
145
|
+
| `client.listen` | `config`, `auth` |
|
|
146
|
+
| `client.webhooks` | `construct_event` (local — no HTTP) |
|
|
147
|
+
|
|
148
|
+
## Sync (Connect)
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
client.payments.create(params, Wajub::RequestOptions.new(sync: 'acct_sync_ref'))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Webhooks
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
begin
|
|
158
|
+
event = client.webhooks.construct_event(
|
|
159
|
+
request.body.read, # String — raw body bytes
|
|
160
|
+
request.env['HTTP_X_WAJUB_SIGNATURE'],
|
|
161
|
+
request.env['HTTP_X_WAJUB_TIMESTAMP']
|
|
162
|
+
)
|
|
163
|
+
rescue Wajub::WebhookSignatureVerificationError
|
|
164
|
+
halt 400
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
case event['type']
|
|
168
|
+
when 'payment.succeeded'
|
|
169
|
+
# fulfill order
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
During local development, use the [Wajub CLI](https://github.com/wajubhq/wajub-cli) to forward webhooks to your machine.
|
|
174
|
+
|
|
175
|
+
## Pagination
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
page = client.payments.list('per_page' => 50)
|
|
179
|
+
|
|
180
|
+
page.auto_paging_each do |payment|
|
|
181
|
+
puts "#{payment['id']} #{payment['status']}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Manual page control
|
|
185
|
+
first = client.payments.list
|
|
186
|
+
second = first.next_page if first.has_more
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Idempotency
|
|
190
|
+
|
|
191
|
+
POST and PUT requests automatically receive an `Idempotency-Key` header. Pass your own:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
client.payments.create(
|
|
195
|
+
params,
|
|
196
|
+
Wajub::RequestOptions.new(idempotency_key: "order-#{order_id}")
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Error handling
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
begin
|
|
204
|
+
client.payments.create(params)
|
|
205
|
+
rescue Wajub::InvalidRequestError => e
|
|
206
|
+
puts e.errors # field-level validation errors
|
|
207
|
+
rescue Wajub::AuthenticationError
|
|
208
|
+
# 401 — bad API key
|
|
209
|
+
rescue Wajub::RateLimitError
|
|
210
|
+
# 429 — back off and retry
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
bundle install
|
|
218
|
+
bundle exec rake test
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Documentation & support
|
|
222
|
+
|
|
223
|
+
- Full API reference: [docs.wajub.com/libraries/sdks/ruby](https://docs.wajub.com/libraries/sdks/ruby)
|
|
224
|
+
- Report issues: [github.com/wajubhq/wajub-ruby/issues](https://github.com/wajubhq/wajub-ruby/issues)
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT — see [LICENSE](LICENSE).
|
data/lib/wajub/client.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'http/http_utils'
|
|
4
|
+
require_relative 'http/base_client'
|
|
5
|
+
require_relative 'resources'
|
|
6
|
+
require_relative 'webhooks'
|
|
7
|
+
|
|
8
|
+
module Wajub
|
|
9
|
+
class Client
|
|
10
|
+
attr_reader :global, :payments, :customers, :refunds, :transfers, :beneficiaries,
|
|
11
|
+
:links, :balance, :events, :accounts, :webhook_endpoints, :invoices,
|
|
12
|
+
:disputes, :identity, :tax, :shield, :listen, :webhooks
|
|
13
|
+
|
|
14
|
+
def initialize(api_key: nil, webhook_secret: nil, idempotency_key_prefix: 'wajub', transport: nil)
|
|
15
|
+
key = HttpUtils.normalize_api_key(api_key || ENV.fetch('WAJUB_API_KEY', ''))
|
|
16
|
+
raise ArgumentError, 'Wajub: api_key is required' if key.empty?
|
|
17
|
+
|
|
18
|
+
base_url = HttpUtils.api_url
|
|
19
|
+
prefix = idempotency_key_prefix
|
|
20
|
+
@transport = transport
|
|
21
|
+
|
|
22
|
+
@global = GlobalResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
23
|
+
@payments = PaymentsResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
24
|
+
@customers = CustomersResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
25
|
+
@refunds = CrudResource.new(key, base_url, 'refunds', 'refund', 'refunds', prefix, transport: shared_transport(base_url))
|
|
26
|
+
@transfers = CrudResource.new(key, base_url, 'transfers', 'transfer', 'transfers', prefix, transport: shared_transport(base_url))
|
|
27
|
+
@beneficiaries = CrudResource.new(key, base_url, 'beneficiaries', 'beneficiary', 'beneficiaries', prefix, transport: shared_transport(base_url))
|
|
28
|
+
@links = CrudResource.new(key, base_url, 'links', 'link', 'links', prefix, transport: shared_transport(base_url))
|
|
29
|
+
@balance = BalanceResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
30
|
+
@events = EventsResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
31
|
+
@accounts = AccountsResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
32
|
+
@webhook_endpoints = WebhookEndpointsResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
33
|
+
@invoices = InvoicesResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
34
|
+
@disputes = DisputesResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
35
|
+
@identity = IdentityResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
36
|
+
@tax = TaxResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
37
|
+
@shield = ShieldResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
38
|
+
@listen = ListenResource.new(key, base_url, prefix, transport: shared_transport(base_url))
|
|
39
|
+
@webhooks = Webhooks.new(webhook_secret || ENV.fetch('WAJUB_WEBHOOK_SECRET', ''))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def shared_transport(base_url)
|
|
45
|
+
@transport || @shared ||= NetHttpTransport.new(base_url, user_agent: "wajub-ruby/#{VERSION}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/wajub/crypto.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module Wajub
|
|
6
|
+
module Crypto
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def hmac_sha256(key, data)
|
|
10
|
+
OpenSSL::HMAC.hexdigest('SHA256', key, data)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def timing_safe_equal(a, b)
|
|
14
|
+
return false unless a.bytesize == b.bytesize
|
|
15
|
+
|
|
16
|
+
l = a.unpack('C*')
|
|
17
|
+
r = b.unpack('C*')
|
|
18
|
+
result = 0
|
|
19
|
+
l.zip(r) { |x, y| result |= x ^ y }
|
|
20
|
+
result.zero?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/wajub/errors.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wajub
|
|
4
|
+
class WajubError < StandardError
|
|
5
|
+
attr_reader :code, :http_status, :errors, :raw
|
|
6
|
+
|
|
7
|
+
def initialize(message, code:, http_status:, errors: nil, raw: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@code = code
|
|
10
|
+
@http_status = http_status
|
|
11
|
+
@errors = errors
|
|
12
|
+
@raw = raw
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class AuthenticationError < WajubError; end
|
|
17
|
+
class PermissionError < WajubError; end
|
|
18
|
+
class NotFoundError < WajubError; end
|
|
19
|
+
class InvalidRequestError < WajubError; end
|
|
20
|
+
|
|
21
|
+
class RateLimitError < WajubError
|
|
22
|
+
attr_reader :retry_after
|
|
23
|
+
|
|
24
|
+
def initialize(message, code:, http_status:, errors: nil, raw: nil, retry_after: nil)
|
|
25
|
+
super(message, code: code, http_status: http_status, errors: errors, raw: raw)
|
|
26
|
+
@retry_after = retry_after
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ApiConnectionError < WajubError
|
|
31
|
+
def initialize(message, code: 'network_error', http_status: 0, errors: nil, raw: nil)
|
|
32
|
+
super(message, code: code, http_status: http_status, errors: errors, raw: raw)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class WebhookSignatureVerificationError < StandardError; end
|
|
37
|
+
|
|
38
|
+
module Errors
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
def from_response(status, body, retry_after: nil)
|
|
42
|
+
message = body['message'] || "Request failed (#{status})"
|
|
43
|
+
code = body['error_code'] || body['code'] || "http_#{status}"
|
|
44
|
+
field_errors = parse_field_errors(body['errors'])
|
|
45
|
+
|
|
46
|
+
case status
|
|
47
|
+
when 401 then AuthenticationError.new(message, code: code, http_status: status, errors: field_errors, raw: body)
|
|
48
|
+
when 403 then PermissionError.new(message, code: code, http_status: status, errors: field_errors, raw: body)
|
|
49
|
+
when 404 then NotFoundError.new(message, code: code, http_status: status, errors: field_errors, raw: body)
|
|
50
|
+
when 429 then RateLimitError.new(message, code: code, http_status: status, errors: field_errors, raw: body, retry_after: retry_after)
|
|
51
|
+
when 400, 422 then InvalidRequestError.new(message, code: code, http_status: status, errors: field_errors, raw: body)
|
|
52
|
+
else WajubError.new(message, code: code, http_status: status, errors: field_errors, raw: body)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_field_errors(raw)
|
|
57
|
+
return nil unless raw.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
raw.transform_values do |value|
|
|
60
|
+
value.is_a?(Array) ? value.first.to_s : value.to_s
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
require_relative 'http_utils'
|
|
8
|
+
require_relative 'retry_policy'
|
|
9
|
+
require_relative '../errors'
|
|
10
|
+
require_relative '../request_options'
|
|
11
|
+
require_relative '../version'
|
|
12
|
+
|
|
13
|
+
module Wajub
|
|
14
|
+
class BaseClient
|
|
15
|
+
attr_reader :api_key, :base_url, :idempotency_key_prefix, :transport, :last_request
|
|
16
|
+
|
|
17
|
+
def initialize(api_key, base_url, idempotency_key_prefix = 'wajub', transport: nil)
|
|
18
|
+
@api_key = HttpUtils.normalize_api_key(api_key)
|
|
19
|
+
@base_url = base_url.sub(%r{/+\z}, '')
|
|
20
|
+
@idempotency_key_prefix = idempotency_key_prefix
|
|
21
|
+
@transport = transport || NetHttpTransport.new(@base_url, user_agent: "wajub-ruby/#{VERSION}")
|
|
22
|
+
@last_request = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get(path, params = nil)
|
|
26
|
+
request('GET', path, body: nil, options: nil, query: params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def post(path, body = nil, options = nil)
|
|
30
|
+
request('POST', path, body: body, options: options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def put(path, body = nil, options = nil)
|
|
34
|
+
request('PUT', path, body: body, options: options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(path, options = nil)
|
|
38
|
+
request('DELETE', path, body: nil, options: options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def request(method, path, body: nil, options: nil, query: nil)
|
|
42
|
+
headers = {
|
|
43
|
+
'Accept' => 'application/json',
|
|
44
|
+
'Authorization' => @api_key
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
options ||= RequestOptions.new
|
|
48
|
+
headers.merge!(options.headers) if options.headers
|
|
49
|
+
headers['X-Sync'] = options.sync if options.sync
|
|
50
|
+
|
|
51
|
+
idempotency_key = options.idempotency_key
|
|
52
|
+
if idempotency_key.nil? && !%w[GET DELETE].include?(method)
|
|
53
|
+
idempotency_key = HttpUtils.create_idempotency_key(@idempotency_key_prefix)
|
|
54
|
+
end
|
|
55
|
+
headers['Idempotency-Key'] = idempotency_key if idempotency_key
|
|
56
|
+
|
|
57
|
+
payload = body.nil? ? nil : JSON.generate(body)
|
|
58
|
+
headers['Content-Type'] = 'application/json' if payload
|
|
59
|
+
|
|
60
|
+
attempt = 0
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
begin
|
|
64
|
+
response = @transport.call(
|
|
65
|
+
method: method,
|
|
66
|
+
path: path,
|
|
67
|
+
headers: headers,
|
|
68
|
+
body: payload,
|
|
69
|
+
query: query
|
|
70
|
+
)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
if attempt < RetryPolicy.max_retries
|
|
73
|
+
attempt += 1
|
|
74
|
+
sleep RetryPolicy.delay_seconds(attempt)
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
raise ApiConnectionError.new(e.message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@last_request = response[:request]
|
|
81
|
+
status = response[:status]
|
|
82
|
+
data = response[:body].is_a?(Hash) ? response[:body] : HttpUtils.decode_json(response[:body].to_s)
|
|
83
|
+
|
|
84
|
+
return data if status >= 200 && status < 300
|
|
85
|
+
|
|
86
|
+
if RetryPolicy.should_retry_status?(status) && attempt < RetryPolicy.max_retries
|
|
87
|
+
attempt += 1
|
|
88
|
+
sleep RetryPolicy.delay_seconds(attempt, response: response[:headers] || {})
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
retry_after = response.dig(:headers, 'retry-after')&.to_i
|
|
93
|
+
raise Errors.from_response(status, data, retry_after: retry_after)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class NetHttpTransport
|
|
99
|
+
def initialize(base_url, user_agent:)
|
|
100
|
+
@base_url = base_url
|
|
101
|
+
@user_agent = user_agent
|
|
102
|
+
@mutex = Mutex.new
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def call(method:, path:, headers:, body:, query:)
|
|
106
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
107
|
+
if query
|
|
108
|
+
filtered = query.compact
|
|
109
|
+
uri.query = URI.encode_www_form(filtered) unless filtered.empty?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
http = connection(uri)
|
|
113
|
+
|
|
114
|
+
klass = Net::HTTP.const_get(method.capitalize)
|
|
115
|
+
request = klass.new(uri)
|
|
116
|
+
headers.each { |k, v| request[k] = v }
|
|
117
|
+
request['User-Agent'] = @user_agent
|
|
118
|
+
request.body = body if body
|
|
119
|
+
|
|
120
|
+
response = http.request(request)
|
|
121
|
+
{
|
|
122
|
+
status: response.code.to_i,
|
|
123
|
+
body: HttpUtils.decode_json(response.body),
|
|
124
|
+
headers: response.to_hash.transform_values(&:first),
|
|
125
|
+
request: {
|
|
126
|
+
method: method,
|
|
127
|
+
path: path,
|
|
128
|
+
uri: uri.to_s,
|
|
129
|
+
headers: headers.merge('User-Agent' => @user_agent)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Reuses a single keep-alive Net::HTTP connection across calls (instead of
|
|
137
|
+
# opening a fresh TCP+TLS connection per request) since every call on this
|
|
138
|
+
# transport targets the same host/port (derived from @base_url).
|
|
139
|
+
def connection(uri)
|
|
140
|
+
@mutex.synchronize do
|
|
141
|
+
if @http && !@http.started?
|
|
142
|
+
@http = nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@http ||= begin
|
|
146
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
147
|
+
http.use_ssl = uri.scheme == 'https'
|
|
148
|
+
http.open_timeout = 30
|
|
149
|
+
http.read_timeout = 30
|
|
150
|
+
http.start
|
|
151
|
+
http
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class MockTransport
|
|
158
|
+
attr_reader :last_request
|
|
159
|
+
|
|
160
|
+
def initialize(responses)
|
|
161
|
+
@responses = responses
|
|
162
|
+
@index = 0
|
|
163
|
+
@last_request = nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def call(method:, path:, headers:, body:, query:)
|
|
167
|
+
@last_request = { method: method, path: path, headers: headers, body: body, query: query }
|
|
168
|
+
response = @responses[@index] || { status: 200, body: {} }
|
|
169
|
+
@index += 1
|
|
170
|
+
response.merge(request: @last_request)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Wajub
|
|
7
|
+
module HttpUtils
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def normalize_api_key(raw)
|
|
11
|
+
key = raw.to_s.strip
|
|
12
|
+
key = key.sub(/\Abearer\s+/i, '').strip if key.match?(/\Abearer\s+/i)
|
|
13
|
+
key
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def api_url
|
|
17
|
+
API_URL
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_idempotency_key(prefix = 'wajub')
|
|
21
|
+
"#{prefix}-#{SecureRandom.uuid}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pick_resource(body, *keys)
|
|
25
|
+
keys.each do |key|
|
|
26
|
+
return body[key] if body.key?(key) && !body[key].nil?
|
|
27
|
+
end
|
|
28
|
+
body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def pick_list(body, *keys)
|
|
32
|
+
keys.each do |key|
|
|
33
|
+
if body.key?(key) && !body[key].nil?
|
|
34
|
+
return { data: body[key], meta: body['meta'] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
if body.key?('items') && !body['items'].nil?
|
|
38
|
+
return { data: body['items'], meta: body['meta'] }
|
|
39
|
+
end
|
|
40
|
+
{ data: body['data'] || [], meta: body['meta'] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def decode_json(data)
|
|
44
|
+
JSON.parse(data)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wajub
|
|
4
|
+
module RetryPolicy
|
|
5
|
+
MAX_RETRIES = 2
|
|
6
|
+
RETRYABLE = [429, 500, 502, 503, 504].freeze
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def should_retry_status?(status)
|
|
11
|
+
RETRYABLE.include?(status)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def max_retries
|
|
15
|
+
MAX_RETRIES
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delay_seconds(attempt, response: nil)
|
|
19
|
+
header = response&.[]('retry-after')&.strip
|
|
20
|
+
if header && !header.empty?
|
|
21
|
+
seconds = Integer(header, exception: false)
|
|
22
|
+
return seconds if seconds
|
|
23
|
+
|
|
24
|
+
time = Time.httpdate(header) rescue nil
|
|
25
|
+
return [(time - Time.now).to_i, 0].max if time
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
0.5 * (2**(attempt - 1))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wajub
|
|
4
|
+
class ApiObject
|
|
5
|
+
def initialize(data)
|
|
6
|
+
@data = data.transform_keys(&:to_s)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def [](key)
|
|
10
|
+
@data[key.to_s]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
@data.dup
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def method_missing(name, *args)
|
|
18
|
+
return super if args.any? || name.end_with?('=')
|
|
19
|
+
|
|
20
|
+
key = name.to_s
|
|
21
|
+
return @data[key] if @data.key?(key)
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(name, include_private = false)
|
|
27
|
+
@data.key?(name.to_s) || super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Payment < ApiObject
|
|
32
|
+
def id
|
|
33
|
+
self['id']
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def status
|
|
37
|
+
self['status']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def authorization_url
|
|
41
|
+
self['authorization_url']
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authorization_token
|
|
45
|
+
self['authorization_token']
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wajub
|
|
4
|
+
class PagedResult
|
|
5
|
+
attr_reader :data, :meta, :has_more
|
|
6
|
+
|
|
7
|
+
def initialize(data:, meta:, has_more:, fetch_page:)
|
|
8
|
+
@data = data
|
|
9
|
+
@meta = meta
|
|
10
|
+
@has_more = has_more
|
|
11
|
+
@fetch_page = fetch_page
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_page
|
|
15
|
+
raise WajubError.new('no more pages available', code: 'pagination_error', http_status: 0) unless @has_more
|
|
16
|
+
|
|
17
|
+
current = @meta&.fetch('current_page', 1).to_i
|
|
18
|
+
@fetch_page.call(current + 1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def auto_paging_each(&block)
|
|
22
|
+
page = self
|
|
23
|
+
loop do
|
|
24
|
+
page.data.each(&block)
|
|
25
|
+
break unless page.has_more
|
|
26
|
+
|
|
27
|
+
page = page.next_page
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.create(client, path, plural_key, params = nil)
|
|
32
|
+
params ||= {}
|
|
33
|
+
fetch = lambda do |page_num|
|
|
34
|
+
query = params.merge('page' => page_num)
|
|
35
|
+
res = client.get(path, query)
|
|
36
|
+
list = HttpUtils.pick_list(res, plural_key)
|
|
37
|
+
meta = list[:meta]
|
|
38
|
+
has_more = false
|
|
39
|
+
if meta
|
|
40
|
+
current = meta['current_page'].to_i
|
|
41
|
+
last = meta['last_page'].to_i
|
|
42
|
+
has_more = current < last if current.positive? && last.positive?
|
|
43
|
+
end
|
|
44
|
+
new(data: list[:data], meta: meta, has_more: has_more, fetch_page: fetch)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
page = params['page'] || params[:page] || 1
|
|
48
|
+
fetch.call(page.to_i)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wajub
|
|
4
|
+
class RequestOptions
|
|
5
|
+
attr_reader :idempotency_key, :headers, :sync
|
|
6
|
+
|
|
7
|
+
def initialize(idempotency_key: nil, headers: nil, sync: nil)
|
|
8
|
+
@idempotency_key = idempotency_key
|
|
9
|
+
@headers = headers || {}
|
|
10
|
+
@sync = sync
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
require_relative 'http/base_client'
|
|
6
|
+
require_relative 'http/http_utils'
|
|
7
|
+
require_relative 'pagination'
|
|
8
|
+
|
|
9
|
+
module Wajub
|
|
10
|
+
class CrudResource < BaseClient
|
|
11
|
+
def initialize(api_key, base_url, path, singular, plural, idempotency_key_prefix = 'wajub', transport: nil)
|
|
12
|
+
super(api_key, base_url, idempotency_key_prefix, transport: transport)
|
|
13
|
+
@path = path
|
|
14
|
+
@singular = singular
|
|
15
|
+
@plural = plural
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create(params, options = nil)
|
|
19
|
+
HttpUtils.pick_resource(post("/#{@path}", params, options), @singular)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def retrieve(id)
|
|
23
|
+
HttpUtils.pick_resource(get("/#{@path}/#{URI.encode_www_form_component(id)}"), @singular)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list(params = nil)
|
|
27
|
+
PagedResult.create(self, "/#{@path}", @plural, params)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update(id, params, options = nil)
|
|
31
|
+
HttpUtils.pick_resource(put("/#{@path}/#{URI.encode_www_form_component(id)}", params, options), @singular)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete(id, options = nil)
|
|
35
|
+
request('DELETE', "/#{@path}/#{URI.encode_www_form_component(id)}", options: options)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class PaymentsResource < BaseClient
|
|
40
|
+
def create(params, options = nil)
|
|
41
|
+
to_payment_object(post('/payments', params, options))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize_payment(params, options = nil)
|
|
45
|
+
create(params, options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def retrieve(id)
|
|
49
|
+
res = get("/payments/#{URI.encode_www_form_component(id)}")
|
|
50
|
+
to_payment_object('transaction' => HttpUtils.pick_resource(res, 'transaction'))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def list(params = nil)
|
|
54
|
+
PagedResult.create(self, '/payments', 'transactions', params)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cancel(id, options = nil)
|
|
58
|
+
res = delete("/payments/#{URI.encode_www_form_component(id)}", options)
|
|
59
|
+
to_payment_object('transaction' => HttpUtils.pick_resource(res, 'transaction'))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process(id, params, options = nil)
|
|
63
|
+
res = post("/payments/#{URI.encode_www_form_component(id)}", params, options)
|
|
64
|
+
to_payment_object('transaction' => HttpUtils.pick_resource(res, 'transaction'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def process_split(id, params, options = nil)
|
|
68
|
+
res = post("/payments/#{URI.encode_www_form_component(id)}/splits", params, options)
|
|
69
|
+
to_payment_object('transaction' => HttpUtils.pick_resource(res, 'transaction'))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def list_refunds(id, params = nil)
|
|
73
|
+
list = HttpUtils.pick_list(get("/payments/#{URI.encode_www_form_component(id)}/refunds", params), 'refunds')
|
|
74
|
+
[list[:data], list[:meta]]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def to_payment_object(body)
|
|
80
|
+
transaction = HttpUtils.pick_resource(body, 'transaction')
|
|
81
|
+
Payment.new(
|
|
82
|
+
transaction.merge(
|
|
83
|
+
'authorization_token' => body['authorization_token'] || transaction['authorization_token'] || transaction['id'],
|
|
84
|
+
'authorization_url' => body['authorization_url'] || transaction['authorization_url'] || ''
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class GlobalResource < BaseClient
|
|
91
|
+
def ping
|
|
92
|
+
get('/')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def channels(params = nil)
|
|
96
|
+
list = HttpUtils.pick_list(get('/channels', params), 'channels')
|
|
97
|
+
[list[:data], list[:meta]]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def countries(params = nil)
|
|
101
|
+
list = HttpUtils.pick_list(get('/countries', params), 'countries')
|
|
102
|
+
[list[:data], list[:meta]]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def currencies(params = nil)
|
|
106
|
+
list = HttpUtils.pick_list(get('/currencies', params), 'currencies')
|
|
107
|
+
[list[:data], list[:meta]]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class BalanceResource < BaseClient
|
|
112
|
+
def retrieve
|
|
113
|
+
HttpUtils.pick_resource(get('/balance'), 'balance', 'data')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class EventsResource < BaseClient
|
|
118
|
+
def list(params = nil)
|
|
119
|
+
PagedResult.create(self, '/events', 'events', params)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def retrieve(id)
|
|
123
|
+
HttpUtils.pick_resource(get("/events/#{URI.encode_www_form_component(id)}"), 'event')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resend(id, options = nil)
|
|
127
|
+
HttpUtils.pick_resource(post("/events/#{URI.encode_www_form_component(id)}/resend", nil, options), 'event')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class CustomersResource < CrudResource
|
|
132
|
+
def initialize(api_key, base_url, idempotency_key_prefix = 'wajub', transport: nil)
|
|
133
|
+
super(api_key, base_url, 'customers', 'customer', 'customers', idempotency_key_prefix, transport: transport)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def block(id, params = nil, options = nil)
|
|
137
|
+
HttpUtils.pick_resource(post("/customers/#{URI.encode_www_form_component(id)}/block", params, options), 'customer')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def unblock(id, options = nil)
|
|
141
|
+
HttpUtils.pick_resource(post("/customers/#{URI.encode_www_form_component(id)}/unblock", nil, options), 'customer')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def activate(id, options = nil)
|
|
145
|
+
HttpUtils.pick_resource(post("/customers/#{URI.encode_www_form_component(id)}/activate", nil, options), 'customer')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def deactivate(id, options = nil)
|
|
149
|
+
HttpUtils.pick_resource(post("/customers/#{URI.encode_www_form_component(id)}/deactivate", nil, options), 'customer')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def list_tax_ids(customer_id)
|
|
153
|
+
list = HttpUtils.pick_list(get("/customers/#{URI.encode_www_form_component(customer_id)}/tax_ids"), 'tax_ids')
|
|
154
|
+
[list[:data], list[:meta]]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def create_tax_id(customer_id, params, options = nil)
|
|
158
|
+
HttpUtils.pick_resource(post("/customers/#{URI.encode_www_form_component(customer_id)}/tax_ids", params, options), 'tax_id')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def delete_tax_id(customer_id, tax_id, options = nil)
|
|
162
|
+
delete("/customers/#{URI.encode_www_form_component(customer_id)}/tax_ids/#{URI.encode_www_form_component(tax_id)}", options)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
class AccountsResource < CrudResource
|
|
167
|
+
def initialize(api_key, base_url, idempotency_key_prefix = 'wajub', transport: nil)
|
|
168
|
+
super(api_key, base_url, 'accounts', 'account', 'accounts', idempotency_key_prefix, transport: transport)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def regenerate_token(id, options = nil)
|
|
172
|
+
HttpUtils.pick_resource(post("/accounts/#{URI.encode_www_form_component(id)}/token", nil, options), 'account')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class WebhookEndpointsResource < CrudResource
|
|
177
|
+
def initialize(api_key, base_url, idempotency_key_prefix = 'wajub', transport: nil)
|
|
178
|
+
super(api_key, base_url, 'webhooks', 'endpoint', 'endpoints', idempotency_key_prefix, transport: transport)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def rotate_secret(id, options = nil)
|
|
182
|
+
HttpUtils.pick_resource(post("/webhooks/#{URI.encode_www_form_component(id)}/rotate-secret", nil, options), 'endpoint')
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class InvoicesResource < CrudResource
|
|
187
|
+
def initialize(api_key, base_url, idempotency_key_prefix = 'wajub', transport: nil)
|
|
188
|
+
super(api_key, base_url, 'invoices', 'invoice', 'invoices', idempotency_key_prefix, transport: transport)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def send(id, options = nil)
|
|
192
|
+
HttpUtils.pick_resource(post("/invoices/#{URI.encode_www_form_component(id)}/send", nil, options), 'invoice')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def mark_paid(id, params = nil, options = nil)
|
|
196
|
+
HttpUtils.pick_resource(post("/invoices/#{URI.encode_www_form_component(id)}/mark-paid", params, options), 'invoice')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def cancel(id, options = nil)
|
|
200
|
+
HttpUtils.pick_resource(post("/invoices/#{URI.encode_www_form_component(id)}/cancel", nil, options), 'invoice')
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
class DisputesResource < BaseClient
|
|
205
|
+
def list(params = nil)
|
|
206
|
+
PagedResult.create(self, '/disputes', 'disputes', params)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def retrieve(id)
|
|
210
|
+
HttpUtils.pick_resource(get("/disputes/#{URI.encode_www_form_component(id)}"), 'dispute')
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def submit_evidence(id, params, options = nil)
|
|
214
|
+
HttpUtils.pick_resource(post("/disputes/#{URI.encode_www_form_component(id)}/submit-evidence", params, options), 'dispute')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def accept(id, options = nil)
|
|
218
|
+
HttpUtils.pick_resource(post("/disputes/#{URI.encode_www_form_component(id)}/accept", nil, options), 'dispute')
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def close(id, options = nil)
|
|
222
|
+
HttpUtils.pick_resource(post("/disputes/#{URI.encode_www_form_component(id)}/close", nil, options), 'dispute')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def send_message(id, params, options = nil)
|
|
226
|
+
HttpUtils.pick_resource(post("/disputes/#{URI.encode_www_form_component(id)}/messages", params, options), 'dispute')
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
class IdentityResource < BaseClient
|
|
231
|
+
def resolve(params, options = nil)
|
|
232
|
+
HttpUtils.pick_resource(post('/identity/resolve', params, options), 'identity')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def validate(params, options = nil)
|
|
236
|
+
HttpUtils.pick_resource(post('/identity/validate', params, options), 'identity')
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class TaxResource < BaseClient
|
|
241
|
+
def get_settings
|
|
242
|
+
HttpUtils.pick_resource(get('/tax/settings'), 'tax', 'settings')
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def update_settings(params, options = nil)
|
|
246
|
+
HttpUtils.pick_resource(put('/tax/settings', params, options), 'tax', 'settings')
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def rates(params = nil)
|
|
250
|
+
list = HttpUtils.pick_list(get('/tax/rates', params), 'rates', 'tax_rates')
|
|
251
|
+
[list[:data], list[:meta]]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def calculate(params, options = nil)
|
|
255
|
+
HttpUtils.pick_resource(post('/tax/calculate', params, options), 'tax', 'calculation')
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
class ShieldResource < BaseClient
|
|
260
|
+
def get_settings
|
|
261
|
+
HttpUtils.pick_resource(get('/shield/settings'), 'shield', 'settings')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def update_settings(params, options = nil)
|
|
265
|
+
HttpUtils.pick_resource(put('/shield/settings', params, options), 'shield', 'settings')
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def stats(params = nil)
|
|
269
|
+
HttpUtils.pick_resource(get('/shield/stats', params), 'shield', 'stats')
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
class ListenResource < BaseClient
|
|
274
|
+
def config
|
|
275
|
+
HttpUtils.pick_resource(get('/listen/config'), 'config', 'listen')
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def auth(params, options = nil)
|
|
279
|
+
HttpUtils.pick_resource(post('/listen/auth', params, options), 'auth', 'listen')
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
require_relative 'crypto'
|
|
6
|
+
require_relative 'errors'
|
|
7
|
+
|
|
8
|
+
module Wajub
|
|
9
|
+
class Webhooks
|
|
10
|
+
DEFAULT_TOLERANCE = 300
|
|
11
|
+
|
|
12
|
+
attr_reader :secret
|
|
13
|
+
|
|
14
|
+
def initialize(secret)
|
|
15
|
+
@secret = secret.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def construct_event(payload, signature, timestamp, tolerance = DEFAULT_TOLERANCE)
|
|
19
|
+
raise ArgumentError, 'Wajub: webhook_secret is required for construct_event' if @secret.empty?
|
|
20
|
+
raise WebhookSignatureVerificationError, 'Invalid webhook signature format. Expected v1={hash}.' unless signature.start_with?('v1=')
|
|
21
|
+
raise ArgumentError, 'Wajub: payload must be the raw request body as a String, not a parsed object' unless payload.is_a?(String)
|
|
22
|
+
|
|
23
|
+
body = payload
|
|
24
|
+
expected = Crypto.hmac_sha256(@secret, "#{timestamp}.#{body}")
|
|
25
|
+
received = signature.delete_prefix('v1=')
|
|
26
|
+
|
|
27
|
+
raise WebhookSignatureVerificationError, 'Webhook signature verification failed.' unless Crypto.timing_safe_equal(expected, received)
|
|
28
|
+
|
|
29
|
+
ts = Float(timestamp)
|
|
30
|
+
raise WebhookSignatureVerificationError, 'Invalid webhook timestamp.' unless ts.finite?
|
|
31
|
+
|
|
32
|
+
drift = (Time.now.to_i - ts).abs
|
|
33
|
+
if tolerance.positive? && drift > tolerance
|
|
34
|
+
raise WebhookSignatureVerificationError,
|
|
35
|
+
"Timestamp outside tolerance zone (#{drift.to_i}s drift, allowed #{tolerance}s)."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
event = JSON.parse(body)
|
|
39
|
+
raise WebhookSignatureVerificationError, 'Invalid webhook payload JSON.' unless event.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
event
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/wajub.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'wajub/version'
|
|
4
|
+
require_relative 'wajub/errors'
|
|
5
|
+
require_relative 'wajub/request_options'
|
|
6
|
+
require_relative 'wajub/objects'
|
|
7
|
+
require_relative 'wajub/client'
|
|
8
|
+
require_relative 'wajub/webhooks'
|
|
9
|
+
|
|
10
|
+
module Wajub
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wajub
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Wajub
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Server-side Wajub payments, webhooks, and merchant API client.
|
|
13
|
+
email:
|
|
14
|
+
- hello@wajub.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/wajub.rb
|
|
22
|
+
- lib/wajub/client.rb
|
|
23
|
+
- lib/wajub/crypto.rb
|
|
24
|
+
- lib/wajub/errors.rb
|
|
25
|
+
- lib/wajub/http/base_client.rb
|
|
26
|
+
- lib/wajub/http/http_utils.rb
|
|
27
|
+
- lib/wajub/http/retry_policy.rb
|
|
28
|
+
- lib/wajub/objects.rb
|
|
29
|
+
- lib/wajub/pagination.rb
|
|
30
|
+
- lib/wajub/request_options.rb
|
|
31
|
+
- lib/wajub/resources.rb
|
|
32
|
+
- lib/wajub/version.rb
|
|
33
|
+
- lib/wajub/webhooks.rb
|
|
34
|
+
homepage: https://docs.wajub.com/libraries/sdks/ruby
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
homepage_uri: https://docs.wajub.com/libraries/sdks/ruby
|
|
39
|
+
source_code_uri: https://github.com/wajubhq/wajub-ruby
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.1'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 4.0.17
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Official Wajub Ruby SDK
|
|
57
|
+
test_files: []
|