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 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
+ [![Gem Version](https://badge.fury.io/rb/payangel.svg)](https://rubygems.org/gems/payangel)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
3
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%202.7-red.svg)](https://www.ruby-lang.org)
4
+ [![GitHub](https://img.shields.io/badge/GitHub-source-black.svg)](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