payangel 1.0.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.txt +21 -0
- data/README.md +549 -0
- data/lib/payangel/client.rb +52 -0
- data/lib/payangel/configuration.rb +51 -0
- data/lib/payangel/errors.rb +40 -0
- data/lib/payangel/http_client.rb +214 -0
- data/lib/payangel/resources/bank_codes.rb +49 -0
- data/lib/payangel/resources/collection.rb +86 -0
- data/lib/payangel/resources/disbursement.rb +142 -0
- data/lib/payangel/response.rb +176 -0
- data/lib/payangel/version.rb +5 -0
- data/lib/payangel/webhook.rb +66 -0
- data/lib/payangel.rb +40 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 78d69482a78e2db741b045780cf31cb172f1f01e340d3742c35cff43915efc25
|
|
4
|
+
data.tar.gz: 5c00884d15bf2553ac5de0464edc868fe6a301c3287a322ac7d94d6447d1229f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2233e1a2dd73d4637c665b69a503ea6054752b7a2731a127f7ccc3d74ba15a8898ce017906cca83b7e39c54f768f77f570c511cf60e46cdf854ed39b838a896e
|
|
7
|
+
data.tar.gz: 88332384adef8033bf82e174bb64fb91799112fc7b93717cc81a50ba90c6b13910de8d9bde1580ac23a20d4f28f555e5e3871ca52610649d162048b27e4edf75
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Charles Agyemang
|
|
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,549 @@
|
|
|
1
|
+
[](https://rubygems.org/gems/payangel)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[](https://www.ruby-lang.org)
|
|
4
|
+
[](https://github.com/charlesagyemang/payangel-ruby-gem)
|
|
5
|
+
|
|
6
|
+
# PayAngel Ruby SDK
|
|
7
|
+
|
|
8
|
+
Official Ruby SDK for the [PayAngel](https://payangel.com) payments API — send and collect money across Africa (Ghana, Nigeria, Kenya, South Africa).
|
|
9
|
+
|
|
10
|
+
**Zero runtime dependencies. Works with Ruby 2.7+.**
|
|
11
|
+
|
|
12
|
+
Built for developers familiar with Stripe, Paystack, and Flutterwave — the API surface will feel immediately familiar.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Authentication](#authentication)
|
|
21
|
+
- [Environments](#environments)
|
|
22
|
+
- [Disbursement](#disbursement)
|
|
23
|
+
- [Collection](#collection)
|
|
24
|
+
- [Webhooks](#webhooks)
|
|
25
|
+
- [Error Handling](#error-handling)
|
|
26
|
+
- [Normalised Response](#normalised-response)
|
|
27
|
+
- [Important Notes](#important-notes)
|
|
28
|
+
- [Known Limitations](#known-limitations)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Add to your Gemfile:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "payangel"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or install directly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
gem install payangel
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require "payangel"
|
|
59
|
+
|
|
60
|
+
client = PayAngel::Client.new(
|
|
61
|
+
public_key: "your_public_key",
|
|
62
|
+
secret_key: "your_secret_key",
|
|
63
|
+
env: "production"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Check your wallet balance
|
|
67
|
+
balance = client.disbursement.balance
|
|
68
|
+
puts balance.data
|
|
69
|
+
# => { "wallets" => [{ "currency" => "USD", "balance" => "1000.00" }] }
|
|
70
|
+
|
|
71
|
+
# Get available banks in Ghana
|
|
72
|
+
banks = client.disbursement.bank_codes(country: "GH")
|
|
73
|
+
banks.data["banks"].each { |b| puts "#{b['payangelCode']} — #{b['name']}" }
|
|
74
|
+
|
|
75
|
+
# Send money to a bank account
|
|
76
|
+
result = client.disbursement.send_money(
|
|
77
|
+
transaction_id: "PAY-001",
|
|
78
|
+
sender_first_name: "John",
|
|
79
|
+
sender_last_name: "Doe",
|
|
80
|
+
country_from: "GH",
|
|
81
|
+
country_to: "GH",
|
|
82
|
+
sending_currency: "GHS",
|
|
83
|
+
receiving_currency: "GHS",
|
|
84
|
+
destination_amount: 100,
|
|
85
|
+
beneficiary_first_name: "Jane",
|
|
86
|
+
beneficiary_last_name: "Mensah",
|
|
87
|
+
transfer_type: "bank",
|
|
88
|
+
bank_account_number: "1234567890",
|
|
89
|
+
bank_code: "170100",
|
|
90
|
+
transfer_reason: "Family Support",
|
|
91
|
+
customer_ref: "REF-001",
|
|
92
|
+
callback_url: "https://yourserver.com/webhooks/payangel"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
puts result.success # => true
|
|
96
|
+
puts result.transaction_id # => "PAY-001"
|
|
97
|
+
puts result.reference_id # => "STA-100000000143"
|
|
98
|
+
puts result.status # => "PENDING"
|
|
99
|
+
puts result.amount # => 100
|
|
100
|
+
puts result.fee # => 0
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Authentication
|
|
106
|
+
|
|
107
|
+
The SDK uses HTTP Basic Auth with your PayAngel API keys from [business.payangel.com](https://business.payangel.com).
|
|
108
|
+
|
|
109
|
+
### Option A: Pass keys directly
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
client = PayAngel::Client.new(
|
|
113
|
+
public_key: "your_public_key",
|
|
114
|
+
secret_key: "your_secret_key",
|
|
115
|
+
env: "production",
|
|
116
|
+
timeout: 30 # optional, in seconds, defaults to 30
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Option B: Use environment variables
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
export PAYANGEL_PUBLIC_KEY=your_public_key
|
|
124
|
+
export PAYANGEL_SECRET_KEY=your_secret_key
|
|
125
|
+
export PAYANGEL_ENV=production
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
client = PayAngel::Client.new # reads from env vars
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Option C: Global configuration
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
PayAngel.configure do |config|
|
|
136
|
+
config.public_key = "your_public_key"
|
|
137
|
+
config.secret_key = "your_secret_key"
|
|
138
|
+
config.env = "production"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
client = PayAngel::Client.new # uses global config
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Config resolution priority
|
|
145
|
+
|
|
146
|
+
1. Explicit constructor arguments
|
|
147
|
+
2. Global configuration (`PayAngel.configure`)
|
|
148
|
+
3. Environment variables (`PAYANGEL_PUBLIC_KEY`, `PAYANGEL_SECRET_KEY`, `PAYANGEL_ENV`)
|
|
149
|
+
4. Defaults (`"production"` for env, `30` for timeout)
|
|
150
|
+
|
|
151
|
+
An `AuthenticationError` is raised at instantiation if no keys are found.
|
|
152
|
+
|
|
153
|
+
### IP Whitelisting
|
|
154
|
+
|
|
155
|
+
PayAngel requires your server IP to be whitelisted. If you receive error code `E0014-098`, add your server's public IP to the whitelist at [business.payangel.com](https://business.payangel.com) under your channel settings.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Environments
|
|
160
|
+
|
|
161
|
+
| Environment | Domain |
|
|
162
|
+
|---|---|
|
|
163
|
+
| Sandbox | `https://payconnect.payangel.org` |
|
|
164
|
+
| Production | `https://payconnect.payangel.com` |
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# Sandbox
|
|
168
|
+
client = PayAngel::Client.new(env: "sandbox", ...)
|
|
169
|
+
|
|
170
|
+
# Production (default)
|
|
171
|
+
client = PayAngel::Client.new(env: "production", ...)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Disbursement
|
|
177
|
+
|
|
178
|
+
### Send Money (Bank Transfer)
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
result = client.disbursement.send_money(
|
|
182
|
+
transaction_id: "PAY-BANK-001",
|
|
183
|
+
sender_first_name: "John",
|
|
184
|
+
sender_last_name: "Doe",
|
|
185
|
+
country_from: "GH",
|
|
186
|
+
country_to: "GH",
|
|
187
|
+
sending_currency: "GHS",
|
|
188
|
+
receiving_currency: "GHS",
|
|
189
|
+
destination_amount: 100,
|
|
190
|
+
beneficiary_first_name: "Jane",
|
|
191
|
+
beneficiary_last_name: "Mensah",
|
|
192
|
+
transfer_type: "bank",
|
|
193
|
+
bank_account_number: "1775702701014",
|
|
194
|
+
bank_name: "FIRST ATLANTIC BANK",
|
|
195
|
+
bank_branch: "EAST LEGON",
|
|
196
|
+
bank_code: "170100",
|
|
197
|
+
transfer_reason: "Family Support",
|
|
198
|
+
customer_ref: "REF-001",
|
|
199
|
+
callback_url: "https://yourserver.com/webhooks/payangel"
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Optional sender fields
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
{
|
|
207
|
+
sender_middlename: "Michael",
|
|
208
|
+
sender_phone: "+1234567890",
|
|
209
|
+
sender_address: "123 Main St",
|
|
210
|
+
sender_city: "New York",
|
|
211
|
+
sender_nationality: "US",
|
|
212
|
+
sender_id_expiry: "2028-12-31",
|
|
213
|
+
source_of_income: "Employment",
|
|
214
|
+
beneficiary_middle_name: "Akua",
|
|
215
|
+
recipient_nationality: "GH",
|
|
216
|
+
receiver_dob: "1990-01-15",
|
|
217
|
+
purpose_details: "Monthly allowance",
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Send Money (Mobile Money)
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
result = client.disbursement.send_money(
|
|
225
|
+
transaction_id: "PAY-MOMO-001",
|
|
226
|
+
sender_first_name: "John",
|
|
227
|
+
sender_last_name: "Doe",
|
|
228
|
+
country_from: "GH",
|
|
229
|
+
country_to: "GH",
|
|
230
|
+
sending_currency: "GHS",
|
|
231
|
+
receiving_currency: "GHS",
|
|
232
|
+
destination_amount: 50,
|
|
233
|
+
beneficiary_first_name: "Kwame",
|
|
234
|
+
beneficiary_last_name: "Asante",
|
|
235
|
+
transfer_type: "mobile",
|
|
236
|
+
mobile_network: "MTN", # accepts any casing: "mtn", "MTN", "Mtn"
|
|
237
|
+
mobile_number: "0551234567",
|
|
238
|
+
transfer_reason: "Gift",
|
|
239
|
+
customer_ref: "REF-002",
|
|
240
|
+
callback_url: "https://yourserver.com/webhooks/payangel"
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Supported mobile networks
|
|
245
|
+
|
|
246
|
+
| Input (any casing) | Disbursement sends | Collection sends |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `"mtn"` / `"MTN"` | `"MTN"` | `"mtn"` |
|
|
249
|
+
| `"telecel"` / `"vodafone"` | `"Telecel"` | `"telecel"` |
|
|
250
|
+
| `"airteltigo"` / `"at"` | `"AirtelTigo"` | `"at"` |
|
|
251
|
+
|
|
252
|
+
The SDK normalises `mobile_network` automatically.
|
|
253
|
+
|
|
254
|
+
### Query Transaction
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
status = client.disbursement.query("PAY-BANK-001")
|
|
258
|
+
|
|
259
|
+
puts status.status # => "SUCCESS"
|
|
260
|
+
puts status.transaction_id # => "PAY-BANK-001"
|
|
261
|
+
puts status.amount # => 100
|
|
262
|
+
puts status.fee # => 0
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Account Balance
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
balance = client.disbursement.balance
|
|
269
|
+
puts balance.data
|
|
270
|
+
# => { "wallets" => [{ "currency" => "USD", "balance" => "1000.00" }] }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Bank Codes
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
codes = client.disbursement.bank_codes(country: "GH")
|
|
277
|
+
|
|
278
|
+
# Banks
|
|
279
|
+
codes.data["banks"].each do |bank|
|
|
280
|
+
puts "#{bank['payangelCode']} — #{bank['name']}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Mobile providers
|
|
284
|
+
codes.data["mobile"].each do |provider|
|
|
285
|
+
puts "#{provider['payangelCode']} — #{provider['name']}"
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Name Check
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
check = client.disbursement.name_check(
|
|
293
|
+
country: "GH",
|
|
294
|
+
account_number: "1775702701014",
|
|
295
|
+
payangel_code: "170100",
|
|
296
|
+
account_type: "bank"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
puts check.success # => true
|
|
300
|
+
puts check.name # => "JANE MENSAH"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Collection
|
|
306
|
+
|
|
307
|
+
### Mobile Money Collection
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
result = client.collection.mobile_money(
|
|
311
|
+
mobile_network: "mtn",
|
|
312
|
+
ip_address: "154.160.2.56",
|
|
313
|
+
transaction_id: "COLL-MOMO-001",
|
|
314
|
+
country: "GH",
|
|
315
|
+
amount: 5,
|
|
316
|
+
customer_account: "233541348180", # must include country code
|
|
317
|
+
currency: "GHS",
|
|
318
|
+
item_description: "Order #1042",
|
|
319
|
+
callback_url: "https://yourserver.com/webhooks/payangel"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
puts result.success # => true
|
|
323
|
+
puts result.transaction_id # => "COLL-MOMO-001"
|
|
324
|
+
puts result.status # => "PENDING"
|
|
325
|
+
puts result.amount # => 5
|
|
326
|
+
puts result.currency # => "GHS"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Card Collection
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
result = client.collection.card(
|
|
333
|
+
transaction_id: "COLL-CARD-001",
|
|
334
|
+
country: "GH",
|
|
335
|
+
customer_account: "customer@example.com",
|
|
336
|
+
currency: "GHS",
|
|
337
|
+
amount: 100,
|
|
338
|
+
item_description: "Premium subscription",
|
|
339
|
+
redirect_url: "https://yoursite.com/payment/complete"
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Collection Status
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
status = client.collection.status("COLL-MOMO-001")
|
|
347
|
+
puts status.status # => "SUCCESS" or "PENDING"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Webhooks
|
|
353
|
+
|
|
354
|
+
Verify webhook signatures using HMAC-SHA256 with timing-safe comparison.
|
|
355
|
+
|
|
356
|
+
### Rails example
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# config/routes.rb
|
|
360
|
+
post "/webhooks/payangel", to: "webhooks#payangel"
|
|
361
|
+
|
|
362
|
+
# app/controllers/webhooks_controller.rb
|
|
363
|
+
class WebhooksController < ApplicationController
|
|
364
|
+
skip_before_action :verify_authenticity_token, only: :payangel
|
|
365
|
+
|
|
366
|
+
def payangel
|
|
367
|
+
payload = request.body.read
|
|
368
|
+
signature = request.headers["X-PayAngel-Signature"]
|
|
369
|
+
|
|
370
|
+
begin
|
|
371
|
+
event = PayAngel::Webhook.verify(
|
|
372
|
+
payload: payload,
|
|
373
|
+
signature: signature,
|
|
374
|
+
secret: ENV["PAYANGEL_WEBHOOK_SECRET"]
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
case event.type
|
|
378
|
+
when "disbursement.completed"
|
|
379
|
+
handle_completed(event.data)
|
|
380
|
+
when "disbursement.failed"
|
|
381
|
+
handle_failed(event.data)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
head :ok
|
|
385
|
+
rescue PayAngel::WebhookSignatureError
|
|
386
|
+
head :unauthorized
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
private
|
|
391
|
+
|
|
392
|
+
def handle_completed(data)
|
|
393
|
+
# data[:transaction_id], data[:amount], data[:currency], etc.
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def handle_failed(data)
|
|
397
|
+
# data[:failure_reason]
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Sinatra example
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
post "/webhooks/payangel" do
|
|
406
|
+
payload = request.body.read
|
|
407
|
+
signature = request.env["HTTP_X_PAYANGEL_SIGNATURE"]
|
|
408
|
+
|
|
409
|
+
begin
|
|
410
|
+
event = PayAngel::Webhook.verify(
|
|
411
|
+
payload: payload,
|
|
412
|
+
signature: signature,
|
|
413
|
+
secret: ENV["PAYANGEL_WEBHOOK_SECRET"]
|
|
414
|
+
)
|
|
415
|
+
# Handle event...
|
|
416
|
+
status 200
|
|
417
|
+
rescue PayAngel::WebhookSignatureError
|
|
418
|
+
status 401
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Webhook event types
|
|
424
|
+
|
|
425
|
+
| Event | Description |
|
|
426
|
+
|---|---|
|
|
427
|
+
| `disbursement.pending` | Transaction created, awaiting processing |
|
|
428
|
+
| `disbursement.processing` | Transaction is being processed |
|
|
429
|
+
| `disbursement.completed` | Transaction completed successfully |
|
|
430
|
+
| `disbursement.failed` | Transaction failed |
|
|
431
|
+
| `disbursement.cancelled` | Transaction was cancelled |
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Error Handling
|
|
436
|
+
|
|
437
|
+
All errors inherit from `PayAngel::Error` and include structured metadata.
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
begin
|
|
441
|
+
client.disbursement.send_money(...)
|
|
442
|
+
rescue PayAngel::AuthenticationError => e
|
|
443
|
+
# 401/403 or E0014-098 (IP whitelist)
|
|
444
|
+
puts e.message
|
|
445
|
+
puts e.code
|
|
446
|
+
rescue PayAngel::InvalidRequestError => e
|
|
447
|
+
# 400/422 — validation errors
|
|
448
|
+
puts e.message
|
|
449
|
+
puts e.fields # => [{ "field" => "...", "message" => "..." }]
|
|
450
|
+
rescue PayAngel::RateLimitError => e
|
|
451
|
+
# 429 — SDK auto-retries 3x with exponential backoff
|
|
452
|
+
puts "Rate limited after retries"
|
|
453
|
+
rescue PayAngel::TransactionError => e
|
|
454
|
+
# Business logic error (E0016-xxx)
|
|
455
|
+
puts e.code, e.message
|
|
456
|
+
rescue PayAngel::ServiceUnavailableError => e
|
|
457
|
+
# 500/503
|
|
458
|
+
puts "Service down"
|
|
459
|
+
rescue PayAngel::Error => e
|
|
460
|
+
# Catch-all
|
|
461
|
+
puts e.http_status, e.message, e.raw
|
|
462
|
+
end
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Error properties
|
|
466
|
+
|
|
467
|
+
| Property | Type | Description |
|
|
468
|
+
|---|---|---|
|
|
469
|
+
| `message` | `String` | Human-readable description |
|
|
470
|
+
| `http_status` | `Integer` | HTTP status code (0 for non-HTTP errors) |
|
|
471
|
+
| `code` | `String` or `nil` | PayAngel error code (e.g. `"E0016-007"`) |
|
|
472
|
+
| `fields` | `Array<Hash>` | Per-field validation errors |
|
|
473
|
+
| `raw` | `Hash` or `nil` | Original unmodified API response |
|
|
474
|
+
|
|
475
|
+
### Automatic retry
|
|
476
|
+
|
|
477
|
+
The SDK automatically retries on `429 Too Many Requests` with exponential backoff:
|
|
478
|
+
|
|
479
|
+
- Attempt 1: wait ~1s
|
|
480
|
+
- Attempt 2: wait ~2s
|
|
481
|
+
- Attempt 3: wait ~4s
|
|
482
|
+
- Attempt 4: raise `RateLimitError`
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Normalised Response
|
|
487
|
+
|
|
488
|
+
Every method returns a `PayAngel::Response` with consistent attributes:
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
response.success # Boolean
|
|
492
|
+
response.transaction_id # String or nil
|
|
493
|
+
response.reference_id # String or nil
|
|
494
|
+
response.status # String or nil ("PENDING", "SUCCESS", "FAILED", etc.)
|
|
495
|
+
response.amount # Numeric or nil
|
|
496
|
+
response.fee # Numeric or nil
|
|
497
|
+
response.total # Numeric or nil
|
|
498
|
+
response.currency # String or nil
|
|
499
|
+
response.created_at # String or nil
|
|
500
|
+
response.data # Hash or nil — typed payload specific to the method
|
|
501
|
+
response.raw # Hash — original API response, unmodified
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Important Notes
|
|
507
|
+
|
|
508
|
+
### `destination_amount` is what the recipient gets
|
|
509
|
+
|
|
510
|
+
`destination_amount` is the amount the **recipient receives**. Your wallet debit = destination_amount + fee + FX spread.
|
|
511
|
+
|
|
512
|
+
### `bank_code` = `payangel_code`
|
|
513
|
+
|
|
514
|
+
The `bank_code` param in `send_money` expects the `payangelCode` from `bank_codes`. Different names, same value.
|
|
515
|
+
|
|
516
|
+
### `customer_account` format for collection
|
|
517
|
+
|
|
518
|
+
Must include the country code:
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
customer_account: "233541348180" # 233 = Ghana
|
|
522
|
+
# NOT "0541348180"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### `callback_url` is required
|
|
526
|
+
|
|
527
|
+
Both disbursement and collection require a callback URL for webhook notifications.
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Known Limitations
|
|
532
|
+
|
|
533
|
+
| # | Question | Current SDK behaviour |
|
|
534
|
+
|---|----------|----------------------|
|
|
535
|
+
| 1 | Does Collection use `Basic` or `Bearer` auth? | Uses `Basic`. One-line change if Bearer needed. |
|
|
536
|
+
| 2 | Does per-request `callback_url` override portal URL? | Assumed yes. |
|
|
537
|
+
| 3 | Does `Retry-After` header exist on 429? | SDK uses fixed exponential backoff. |
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Contributing
|
|
542
|
+
|
|
543
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/charlesagyemang/payangel-ruby-gem).
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
MIT — see [LICENSE.txt](https://github.com/charlesagyemang/payangel-ruby-gem/blob/main/LICENSE.txt).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PayAngel
|
|
4
|
+
# Main client class. Instantiate with API keys and access resources.
|
|
5
|
+
#
|
|
6
|
+
# @example Explicit keys
|
|
7
|
+
# client = PayAngel::Client.new(
|
|
8
|
+
# public_key: "pk_live_...",
|
|
9
|
+
# secret_key: "sk_live_...",
|
|
10
|
+
# env: "production"
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
# @example Environment variables
|
|
14
|
+
# # Set PAYANGEL_PUBLIC_KEY, PAYANGEL_SECRET_KEY, PAYANGEL_ENV
|
|
15
|
+
# client = PayAngel::Client.new
|
|
16
|
+
#
|
|
17
|
+
# @example Access resources
|
|
18
|
+
# client.disbursement.send_money(...)
|
|
19
|
+
# client.disbursement.query(transaction_id)
|
|
20
|
+
# client.disbursement.balance
|
|
21
|
+
# client.collection.mobile_money(...)
|
|
22
|
+
# client.collection.status(transaction_id)
|
|
23
|
+
class Client
|
|
24
|
+
attr_reader :config
|
|
25
|
+
|
|
26
|
+
# @param public_key [String, nil] your PayAngel public key (falls back to PAYANGEL_PUBLIC_KEY)
|
|
27
|
+
# @param secret_key [String, nil] your PayAngel secret key (falls back to PAYANGEL_SECRET_KEY)
|
|
28
|
+
# @param env [String, nil] "sandbox" or "production" (falls back to PAYANGEL_ENV, default "production")
|
|
29
|
+
# @param timeout [Integer, nil] request timeout in seconds (default 30)
|
|
30
|
+
# @raise [PayAngel::AuthenticationError] if keys are missing
|
|
31
|
+
def initialize(public_key: nil, secret_key: nil, env: nil, timeout: nil)
|
|
32
|
+
@config = Configuration.new(
|
|
33
|
+
public_key: public_key,
|
|
34
|
+
secret_key: secret_key,
|
|
35
|
+
env: env,
|
|
36
|
+
timeout: timeout
|
|
37
|
+
)
|
|
38
|
+
@config.validate!
|
|
39
|
+
@http = HttpClient.new(@config)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [PayAngel::Resources::Disbursement]
|
|
43
|
+
def disbursement
|
|
44
|
+
@disbursement ||= Resources::Disbursement.new(@http)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [PayAngel::Resources::Collection]
|
|
48
|
+
def collection
|
|
49
|
+
@collection ||= Resources::Collection.new(@http)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PayAngel
|
|
4
|
+
# Holds resolved configuration for a PayAngel client.
|
|
5
|
+
#
|
|
6
|
+
# Resolution priority:
|
|
7
|
+
# 1. Explicit value passed to constructor / configure block
|
|
8
|
+
# 2. Environment variable
|
|
9
|
+
# 3. Default
|
|
10
|
+
class Configuration
|
|
11
|
+
attr_accessor :public_key, :secret_key, :env, :timeout
|
|
12
|
+
|
|
13
|
+
ENVIRONMENTS = {
|
|
14
|
+
"sandbox" => "https://payconnect.payangel.org",
|
|
15
|
+
"production" => "https://payconnect.payangel.com"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(public_key: nil, secret_key: nil, env: nil, timeout: nil)
|
|
19
|
+
@public_key = public_key || ENV["PAYANGEL_PUBLIC_KEY"]
|
|
20
|
+
@secret_key = secret_key || ENV["PAYANGEL_SECRET_KEY"]
|
|
21
|
+
@env = (env || ENV["PAYANGEL_ENV"] || "production").to_s
|
|
22
|
+
@timeout = (timeout || 30).to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate!
|
|
26
|
+
if @public_key.nil? || @public_key.to_s.strip.empty?
|
|
27
|
+
raise AuthenticationError.new(
|
|
28
|
+
"Missing public key. Pass public_key: to the client or set PAYANGEL_PUBLIC_KEY.",
|
|
29
|
+
http_status: 0
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if @secret_key.nil? || @secret_key.to_s.strip.empty?
|
|
34
|
+
raise AuthenticationError.new(
|
|
35
|
+
"Missing secret key. Pass secret_key: to the client or set PAYANGEL_SECRET_KEY.",
|
|
36
|
+
http_status: 0
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
unless ENVIRONMENTS.key?(@env)
|
|
41
|
+
raise ArgumentError, "env must be 'sandbox' or 'production', got '#{@env}'"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def base_url
|
|
48
|
+
ENVIRONMENTS.fetch(@env)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|