paystack_sdk 0.0.5 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dc8af5f5a8c1305ac52bb936f3601982204a1ccab20ee7d63b2b7ad89c03b3f
4
- data.tar.gz: 30c6c8e89a107566d079ca22f78f56cbb6d48701db2d97f60ddb6357a2f5c5b7
3
+ metadata.gz: d4ff6401efb6a0aba25394b61aa74e1a2645848c015565f3590ffdc10f504748
4
+ data.tar.gz: e59175c14b106866b7129e5cb640ec9a64feebb5a2ddce12940e0436e8dc88af
5
5
  SHA512:
6
- metadata.gz: b5f448b8d57b73b3837542791f56418b1711dc697b7551b1240eac3b86d5337d7a125b70d006654ef41a84e16b40da38ad5bb5178faf50fee12ac72ca9d8c7f1
7
- data.tar.gz: 6c3d0d9eb5c06106e1fea26e7434034a42693925cae29a7cbf9288d5655c48598a91a80cc353b238dfdd9bb73be957d4f2e511fc54004260521aa8fedaf17cf3
6
+ metadata.gz: 964956e0a22d4ea337ac49896a865d43a0d6ba6ac2f2cc307a49f120b0b407cb4dedf80c6f3b5ed8a227b89d283e5a9d2729210c3763d4f6236f373de5e9bc6b
7
+ data.tar.gz: e86ef5b41b26ee645f3f3b208cc945cb382d38d768ace5440af66fc0e82a3fac2e036902723021da0d896b90b4b1877b5f395f3ea2860e577010035624608de7
data/.standard.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
- ruby_version: 3.2.2
3
+ ruby_version: 3.3.5
4
4
  fix: true
5
- format: progress
5
+ format: progress
data/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
- ## [Unreleased]
1
+ # Changelog
2
+
3
+ ## [0.0.6] - 2025-06-10
4
+
5
+ ### Added
6
+
7
+ - Customers resource with full CRUD operations (create, list, fetch, update)
8
+ - Customer validation and risk action management features
9
+ - Customer authorization deactivation functionality
10
+ - Comprehensive documentation for Customers API in README
11
+
12
+ ### Changed
13
+
14
+ - Improved test consistency by standardizing response body format using string keys with hashrocket notation (`=>`) across all test specifications
15
+ - Enhanced error handling specificity in tests by using appropriate error classes (`InvalidValueError`, `InvalidFormatError`, `MissingParamError`) instead of generic `Error` class
16
+
17
+ ### Improved
18
+
19
+ - Better test coverage and reliability with consistent response format handling
20
+ - More precise error validation ensuring proper exception types are raised for different validation failures
21
+ - Enhanced documentation with comprehensive examples for all customer operations
22
+
23
+ ## [0.0.5] - 2025-05-15
24
+
25
+ ### Added
26
+
27
+ - Connection utilities module for improved API connection handling and management
28
+ - Enhanced connection configuration and error handling capabilities
29
+
30
+ ## [0.0.4] - 2025-05-15
31
+
32
+ ### Added
33
+
34
+ - Enhanced transaction resource validations with comprehensive parameter checking
35
+ - New transaction methods: `charge_authorization` and `partial_debit`
36
+ - Flexible `list` method accepting additional parameters for advanced API requests
37
+
38
+ ### Changed
39
+
40
+ - Updated SDK authentication to use `secret_key` instead of `api_key` for consistency with Paystack documentation
41
+ - Improved transaction method names for better clarity and consistency
42
+ - Enhanced README documentation with comprehensive usage examples
43
+
44
+ ### Improved
45
+
46
+ - Better validation error messages and handling
47
+ - More robust parameter validation across all transaction methods
48
+
49
+ ## [0.0.3] - 2025-05-13
50
+
51
+ ### Changed
52
+
53
+ - Renamed `#initialize_transaction` method to `#initiate` for better clarity and consistency with Paystack API
54
+ - Updated .gitignore to exclude Gemfile.lock from version control
55
+
56
+ ### Fixed
57
+
58
+ - Corrected typos in README documentation regarding original API response handling
59
+
60
+ ## [0.0.2] - 2025-05-11
61
+
62
+ ### Added
63
+
64
+ - Comprehensive Response class for Paystack API response handling with dynamic attribute access
65
+ - Enhanced response object capabilities with better data access patterns
66
+ - Improved debugging support for development
67
+
68
+ ### Changed
69
+
70
+ - Refactored transaction specs to utilize PaystackSdk::Response for better consistency
71
+ - Removed redundant success? method in favor of centralized Response handling
72
+ - Enhanced documentation clarity on original API response handling
73
+
74
+ ### Improved
75
+
76
+ - Better response handling architecture across all SDK components
77
+ - More intuitive API for accessing response data and metadata
2
78
 
3
79
  ## [0.0.1] - 2025-05-10
4
80
 
5
- - Initial release
81
+ ### Added
82
+
83
+ - Initial release of Paystack Ruby SDK
84
+ - Basic transaction operations (initiate, verify)
85
+ - Foundational SDK architecture with modular design
86
+ - Comprehensive error handling framework
87
+ - Basic client initialization and configuration
88
+ - Initial documentation and usage examples
data/README.md CHANGED
@@ -14,6 +14,14 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
14
14
  - [List Transactions](#list-transactions)
15
15
  - [Fetch a Transaction](#fetch-a-transaction)
16
16
  - [Get Transaction Totals](#get-transaction-totals)
17
+ - [Customers](#customers)
18
+ - [Create a Customer](#create-a-customer)
19
+ - [List Customers](#list-customers)
20
+ - [Fetch a Customer](#fetch-a-customer)
21
+ - [Update a Customer](#update-a-customer)
22
+ - [Validate a Customer](#validate-a-customer)
23
+ - [Set Risk Action](#set-risk-action)
24
+ - [Deactivate Authorization](#deactivate-authorization)
17
25
  - [Response Handling](#response-handling)
18
26
  - [Working with Response Objects](#working-with-response-objects)
19
27
  - [Accessing the Original Response](#accessing-the-original-response)
@@ -65,11 +73,31 @@ response = paystack.transactions.initiate(params)
65
73
 
66
74
  if response.success?
67
75
  puts "Visit this URL to complete payment: #{response.authorization_url}"
76
+ puts "Transaction reference: #{response.reference}"
68
77
  else
69
78
  puts "Error: #{response.error_message}"
70
79
  end
80
+
81
+ # Create a customer
82
+ customer_params = {
83
+ email: "customer@email.com",
84
+ first_name: "John",
85
+ last_name: "Doe"
86
+ }
87
+
88
+ customer_response = paystack.customers.create(customer_params)
89
+
90
+ if customer_response.success?
91
+ puts "Customer created: #{customer_response.data.customer_code}"
92
+ else
93
+ puts "Error: #{customer_response.error_message}"
94
+ end
71
95
  ```
72
96
 
97
+ ### Response Format
98
+
99
+ The SDK handles API responses that use string keys (as returned by Paystack) and provides seamless access through both string and symbol notation. All response data maintains the original string key format from the API while offering convenient dot notation access.
100
+
73
101
  ## Usage
74
102
 
75
103
  ### Client Initialization
@@ -219,6 +247,165 @@ else
219
247
  end
220
248
  ```
221
249
 
250
+ ### Customers
251
+
252
+ The SDK provides comprehensive support for Paystack's Customer API, allowing you to manage customer records and their associated data.
253
+
254
+ #### Create a Customer
255
+
256
+ ```ruby
257
+ # Prepare customer parameters
258
+ params = {
259
+ email: "customer@example.com",
260
+ first_name: "John",
261
+ last_name: "Doe",
262
+ phone: "+2348123456789"
263
+ }
264
+
265
+ # Create the customer
266
+ response = paystack.customers.create(params)
267
+
268
+ if response.success?
269
+ puts "Customer created successfully!"
270
+ puts "Customer Code: #{response.data.customer_code}"
271
+ puts "Email: #{response.data.email}"
272
+ puts "Name: #{response.data.first_name} #{response.data.last_name}"
273
+ else
274
+ puts "Error: #{response.error_message}"
275
+ end
276
+ ```
277
+
278
+ #### List Customers
279
+
280
+ ```ruby
281
+ # Get all customers (default pagination: 50 per page)
282
+ response = paystack.customers.list
283
+
284
+ # With custom pagination
285
+ response = paystack.customers.list(per_page: 20, page: 2)
286
+
287
+ # With date filters
288
+ response = paystack.customers.list(
289
+ per_page: 10,
290
+ page: 1,
291
+ from: "2025-01-01",
292
+ to: "2025-06-10"
293
+ )
294
+
295
+ if response.success?
296
+ puts "Total customers: #{response.data.size}"
297
+
298
+ response.data.each do |customer|
299
+ puts "Code: #{customer.customer_code}"
300
+ puts "Email: #{customer.email}"
301
+ puts "Name: #{customer.first_name} #{customer.last_name}"
302
+ puts "----------------"
303
+ end
304
+ else
305
+ puts "Error: #{response.error_message}"
306
+ end
307
+ ```
308
+
309
+ #### Fetch a Customer
310
+
311
+ ```ruby
312
+ # Fetch by customer code
313
+ customer_code = "CUS_xr58yrr2ujlft9k"
314
+ response = paystack.customers.fetch(customer_code)
315
+
316
+ # Or fetch by email
317
+ response = paystack.customers.fetch("customer@example.com")
318
+
319
+ if response.success?
320
+ customer = response.data
321
+ puts "Customer details:"
322
+ puts "Code: #{customer.customer_code}"
323
+ puts "Email: #{customer.email}"
324
+ puts "Name: #{customer.first_name} #{customer.last_name}"
325
+ puts "Phone: #{customer.phone}"
326
+ else
327
+ puts "Error: #{response.error_message}"
328
+ end
329
+ ```
330
+
331
+ #### Update a Customer
332
+
333
+ ```ruby
334
+ customer_code = "CUS_xr58yrr2ujlft9k"
335
+ update_params = {
336
+ first_name: "Jane",
337
+ last_name: "Smith",
338
+ phone: "+2348987654321"
339
+ }
340
+
341
+ response = paystack.customers.update(customer_code, update_params)
342
+
343
+ if response.success?
344
+ puts "Customer updated successfully!"
345
+ puts "Updated Name: #{response.data.first_name} #{response.data.last_name}"
346
+ else
347
+ puts "Error: #{response.error_message}"
348
+ end
349
+ ```
350
+
351
+ #### Validate a Customer
352
+
353
+ ```ruby
354
+ customer_code = "CUS_xr58yrr2ujlft9k"
355
+ validation_params = {
356
+ country: "NG",
357
+ type: "bank_account",
358
+ account_number: "0123456789",
359
+ bvn: "20012345677",
360
+ bank_code: "007",
361
+ first_name: "John",
362
+ last_name: "Doe"
363
+ }
364
+
365
+ response = paystack.customers.validate(customer_code, validation_params)
366
+
367
+ if response.success?
368
+ puts "Customer validation initiated: #{response.message}"
369
+ else
370
+ puts "Error: #{response.error_message}"
371
+ end
372
+ ```
373
+
374
+ #### Set Risk Action
375
+
376
+ ```ruby
377
+ params = {
378
+ customer: "CUS_xr58yrr2ujlft9k",
379
+ risk_action: "allow" # Options: "default", "allow", "deny"
380
+ }
381
+
382
+ response = paystack.customers.set_risk_action(params)
383
+
384
+ if response.success?
385
+ puts "Risk action set successfully!"
386
+ puts "Customer: #{response.data.customer_code}"
387
+ puts "Risk Action: #{response.data.risk_action}"
388
+ else
389
+ puts "Error: #{response.error_message}"
390
+ end
391
+ ```
392
+
393
+ #### Deactivate Authorization
394
+
395
+ ```ruby
396
+ params = {
397
+ authorization_code: "AUTH_72btv547"
398
+ }
399
+
400
+ response = paystack.customers.deactivate_authorization(params)
401
+
402
+ if response.success?
403
+ puts "Authorization deactivated: #{response.message}"
404
+ else
405
+ puts "Error: #{response.error_message}"
406
+ end
407
+ ```
408
+
222
409
  ### Response Handling
223
410
 
224
411
  #### Working with Response Objects
@@ -272,7 +459,24 @@ current_page = original.dig("meta", "page")
272
459
 
273
460
  #### Error Handling
274
461
 
462
+ The SDK provides specific error classes for different types of failures, making it easier to handle errors appropriately:
463
+
275
464
  ```ruby
465
+ begin
466
+ response = paystack.transactions.verify(reference: "invalid_reference")
467
+ rescue PaystackSdk::ResourceNotFoundError => e
468
+ puts "Resource not found: #{e.message}"
469
+ rescue PaystackSdk::AuthenticationError => e
470
+ puts "Authentication failed: #{e.message}"
471
+ rescue PaystackSdk::ValidationError => e
472
+ puts "Validation error: #{e.message}"
473
+ rescue PaystackSdk::APIError => e
474
+ puts "API error: #{e.message}"
475
+ rescue PaystackSdk::Error => e
476
+ puts "General error: #{e.message}"
477
+ end
478
+
479
+ # Alternatively, check response success without exceptions
276
480
  response = paystack.transactions.verify(reference: "invalid_reference")
277
481
 
278
482
  unless response.success?
@@ -287,6 +491,22 @@ unless response.success?
287
491
  end
288
492
  ```
289
493
 
494
+ ##### Error Types
495
+
496
+ The SDK includes several specific error classes:
497
+
498
+ - **`PaystackSdk::ValidationError`** - Base class for all validation errors
499
+
500
+ - **`PaystackSdk::MissingParamError`** - Raised when required parameters are missing
501
+ - **`PaystackSdk::InvalidFormatError`** - Raised when parameters have invalid format (e.g., invalid email)
502
+ - **`PaystackSdk::InvalidValueError`** - Raised when parameters have invalid values (e.g., not in allowed list)
503
+
504
+ - **`PaystackSdk::APIError`** - Base class for API-related errors
505
+ - **`PaystackSdk::AuthenticationError`** - Authentication failures
506
+ - **`PaystackSdk::ResourceNotFoundError`** - Resource not found (404 errors)
507
+ - **`PaystackSdk::RateLimitError`** - Rate limiting encountered
508
+ - **`PaystackSdk::ServerError`** - Server errors (5xx responses)
509
+
290
510
  ## Advanced Usage
291
511
 
292
512
  ### Environment Variables
@@ -299,6 +519,7 @@ ENV["PAYSTACK_SECRET_KEY"] = "sk_test_xxx"
299
519
 
300
520
  # Then initialize resources without specifying the key
301
521
  transactions = PaystackSdk::Resources::Transactions.new
522
+ customers = PaystackSdk::Resources::Customers.new
302
523
  ```
303
524
 
304
525
  ### Direct Resource Instantiation
@@ -308,6 +529,7 @@ For more advanced usage, you can instantiate resource classes directly:
308
529
  ```ruby
309
530
  # With a secret key
310
531
  transactions = PaystackSdk::Resources::Transactions.new(secret_key: "sk_test_xxx")
532
+ customers = PaystackSdk::Resources::Customers.new(secret_key: "sk_test_xxx")
311
533
 
312
534
  # With an existing Faraday connection
313
535
  connection = Faraday.new(url: "https://api.paystack.co") do |conn|
@@ -316,6 +538,7 @@ end
316
538
 
317
539
  # The secret key can be omitted if set in an environment
318
540
  transactions = PaystackSdk::Resources::Transactions.new(connection, secret_key:)
541
+ customers = PaystackSdk::Resources::Customers.new(connection, secret_key:)
319
542
  ```
320
543
 
321
544
  For more detailed documentation on specific resources, please refer to the following guides:
@@ -329,6 +552,33 @@ For more detailed documentation on specific resources, please refer to the follo
329
552
 
330
553
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
331
554
 
555
+ ### Testing
556
+
557
+ The SDK includes comprehensive test coverage with consistent response format handling. All test specifications use string keys with hashrocket notation (`=>`) to match the actual format returned by the Paystack API:
558
+
559
+ ```ruby
560
+ # Example test response format
561
+ .and_return(Faraday::Response.new(status: 200, body: {
562
+ "status" => true,
563
+ "message" => "Transaction initialized",
564
+ "data" => {
565
+ "authorization_url" => "https://checkout.paystack.com/abc123",
566
+ "access_code" => "access_code_123",
567
+ "reference" => "ref_123"
568
+ }
569
+ }))
570
+ ```
571
+
572
+ Tests also validate specific error types to ensure proper exception handling:
573
+
574
+ ```ruby
575
+ # Testing specific error types
576
+ expect { customers.set_risk_action(invalid_params) }
577
+ .to raise_error(PaystackSdk::InvalidValueError, /risk_action/i)
578
+ ```
579
+
580
+ ### Installation and Release
581
+
332
582
  To install this gem onto your local machine, run:
333
583
 
334
584
  ```bash
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "resources/transactions"
4
+ require_relative "resources/customers"
4
5
  require_relative "utils/connection_utils"
5
6
 
6
7
  module PaystackSdk
@@ -46,5 +47,19 @@ module PaystackSdk
46
47
  def transactions
47
48
  @transactions ||= Resources::Transactions.new(@connection)
48
49
  end
50
+
51
+ # Provides access to the `Customers` resource.
52
+ #
53
+ # @return [PaystackSdk::Resources::Customers] An instance of the
54
+ # `Customers` resource.
55
+ #
56
+ # @example
57
+ # ```ruby
58
+ # customers = client.customers
59
+ # response = customers.list
60
+ # ```
61
+ def customers
62
+ @customers ||= Resources::Customers.new(@connection)
63
+ end
49
64
  end
50
65
  end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module PaystackSdk
6
+ module Resources
7
+ # The `Customers` class provides methods for interacting with the Paystack Customers API.
8
+ # It allows you to create, list, fetch, update, and manage customers on your integration.
9
+ #
10
+ # Example usage:
11
+ # ```ruby
12
+ # customers = PaystackSdk::Resources::Customers.new(secret_key:)
13
+ #
14
+ # # Create a customer
15
+ # payload = { email: "customer@email.com", first_name: "Zero", last_name: "Sum" }
16
+ # response = customers.create(payload)
17
+ # if response.success?
18
+ # puts "Customer created successfully."
19
+ # puts "Customer code: #{response.customer_code}"
20
+ # else
21
+ # puts "Error creating customer: #{response.error_message}"
22
+ # end
23
+ #
24
+ # # List customers
25
+ # response = customers.list(per_page: 50, page: 1)
26
+ #
27
+ # # Fetch a customer
28
+ # response = customers.fetch("CUS_xxxxx")
29
+ #
30
+ # # Update a customer
31
+ # response = customers.update("CUS_xxxxx", { first_name: "John" })
32
+ # ```
33
+ class Customers < PaystackSdk::Resources::Base
34
+ # Creates a new customer.
35
+ #
36
+ # @param payload [Hash] The payload containing customer details.
37
+ # @option payload [String] :email (required) Customer's email address
38
+ # @option payload [String] :first_name Customer's first name
39
+ # @option payload [String] :last_name Customer's last name
40
+ # @option payload [String] :phone Customer's phone number
41
+ # @option payload [Hash] :metadata Additional customer information
42
+ # @return [PaystackSdk::Response] The response from the Paystack API.
43
+ # @raise [PaystackSdk::Error] If the payload is invalid or the API request fails.
44
+ def create(payload)
45
+ validate_fields!(
46
+ payload: payload,
47
+ validations: {
48
+ email: {type: :email, required: true},
49
+ first_name: {type: :string, required: false},
50
+ last_name: {type: :string, required: false},
51
+ phone: {type: :string, required: false}
52
+ }
53
+ )
54
+
55
+ response = @connection.post("customer", payload)
56
+ handle_response(response)
57
+ end
58
+
59
+ # Lists all customers.
60
+ #
61
+ # @param per_page [Integer] Number of records per page (default: 50)
62
+ # @param page [Integer] Page number to retrieve (default: 1)
63
+ # @param from [String] Start date for filtering
64
+ # @param to [String] End date for filtering
65
+ # @return [PaystackSdk::Response] The response from the Paystack API.
66
+ # @raise [PaystackSdk::Error] If the API request fails.
67
+ def list(per_page: 50, page: 1, **params)
68
+ validate_positive_integer!(value: per_page, name: "per_page", allow_nil: true)
69
+ validate_positive_integer!(value: page, name: "page", allow_nil: true)
70
+
71
+ if params[:from]
72
+ validate_date_format!(date_str: params[:from], name: "from")
73
+ end
74
+
75
+ if params[:to]
76
+ validate_date_format!(date_str: params[:to], name: "to")
77
+ end
78
+
79
+ query_params = {perPage: per_page, page: page}.merge(params)
80
+ response = @connection.get("customer", query_params)
81
+ handle_response(response)
82
+ end
83
+
84
+ # Fetches details of a single customer by email or code.
85
+ #
86
+ # @param email_or_code [String] Email or customer code
87
+ # @return [PaystackSdk::Response] The response from the Paystack API.
88
+ # @raise [PaystackSdk::Error] If the parameter is invalid or the API request fails.
89
+ def fetch(email_or_code)
90
+ validate_presence!(value: email_or_code, name: "email_or_code")
91
+ response = @connection.get("customer/#{email_or_code}")
92
+ handle_response(response)
93
+ end
94
+
95
+ # Updates a customer's details.
96
+ #
97
+ # @param code [String] Customer's code
98
+ # @param payload [Hash] The payload containing customer details to update
99
+ # @option payload [String] :first_name Customer's first name
100
+ # @option payload [String] :last_name Customer's last name
101
+ # @option payload [String] :phone Customer's phone number
102
+ # @option payload [Hash] :metadata Additional customer information
103
+ # @return [PaystackSdk::Response] The response from the Paystack API.
104
+ # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails.
105
+ def update(code, payload)
106
+ validate_presence!(value: code, name: "code")
107
+ validate_hash!(input: payload, name: "payload")
108
+
109
+ response = @connection.put("customer/#{code}", payload)
110
+ handle_response(response)
111
+ end
112
+
113
+ # Validates a customer's identity.
114
+ #
115
+ # @param code [String] Customer's code
116
+ # @param payload [Hash] The payload containing validation details
117
+ # @option payload [String] :country (required) 2 letter country code
118
+ # @option payload [String] :type (required) Type of identification
119
+ # @option payload [String] :account_number Bank account number (required for bank_account type)
120
+ # @option payload [String] :bvn Bank Verification Number
121
+ # @option payload [String] :bank_code Bank code
122
+ # @option payload [String] :first_name Customer's first name
123
+ # @option payload [String] :last_name Customer's last name
124
+ # @return [PaystackSdk::Response] The response from the Paystack API.
125
+ # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails.
126
+ def validate(code, payload)
127
+ validate_presence!(value: code, name: "code")
128
+ validate_fields!(
129
+ payload: payload,
130
+ validations: {
131
+ country: {type: :string, required: true},
132
+ type: {type: :string, required: true},
133
+ account_number: {type: :string, required: true},
134
+ bank_code: {type: :string, required: true}
135
+ }
136
+ )
137
+
138
+ response = @connection.post("customer/#{code}/identification", payload)
139
+ handle_response(response)
140
+ end
141
+
142
+ # Sets the risk action for a customer.
143
+ #
144
+ # @param payload [Hash] The payload containing risk action details
145
+ # @option payload [String] :customer (required) Customer's code or email address
146
+ # @option payload [String] :risk_action (required) Risk action to set ('default', 'allow', or 'deny')
147
+ # @return [PaystackSdk::Response] The response from the Paystack API.
148
+ # @raise [PaystackSdk::MissingParamError] If required parameters are missing
149
+ # @raise [PaystackSdk::InvalidValueError] If risk_action is not one of the allowed values
150
+ # @raise [PaystackSdk::APIError] If the API request fails.
151
+ #
152
+ # @example
153
+ # ```ruby
154
+ # payload = { customer: "CUS_xxxxx", risk_action: "allow" }
155
+ # response = customers.set_risk_action(payload)
156
+ # if response.success?
157
+ # puts "Risk action updated successfully"
158
+ # end
159
+ # ```
160
+ def set_risk_action(payload)
161
+ validate_fields!(
162
+ payload: payload,
163
+ validations: {
164
+ customer: {type: :string, required: true},
165
+ risk_action: {type: :inclusion, required: true, allowed_values: %w[default allow deny]}
166
+ }
167
+ )
168
+
169
+ response = @connection.post("customer/set_risk_action", payload)
170
+ handle_response(response)
171
+ end
172
+
173
+ # Deactivates a customer's authorization.
174
+ #
175
+ # @param payload [Hash] The payload containing authorization details
176
+ # @option payload [String] :authorization_code (required) Authorization code to deactivate
177
+ # @return [PaystackSdk::Response] The response from the Paystack API.
178
+ # @raise [PaystackSdk::Error] If the parameters are invalid or the API request fails.
179
+ def deactivate_authorization(payload)
180
+ validate_fields!(
181
+ payload: payload,
182
+ validations: {
183
+ authorization_code: {type: :string, required: true}
184
+ }
185
+ )
186
+
187
+ response = @connection.post("customer/deactivate_authorization", payload)
188
+ handle_response(response)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -3,12 +3,17 @@ module PaystackSdk
3
3
  # It offers convenient access to response data through dot notation and
4
4
  # supports both direct attribute access and hash/array-like operations.
5
5
  #
6
+ # The Response class handles API responses that use string keys (as returned
7
+ # by the Paystack API) and provides seamless access through both string and
8
+ # symbol notation.
9
+ #
6
10
  # Features:
7
11
  # - Dynamic attribute access via dot notation (response.data.attribute)
8
- # - Hash-like access (response[:key])
12
+ # - Hash-like access (response[:key] or response["key"])
9
13
  # - Array-like access for list responses (response[0])
10
14
  # - Iteration support (response.each)
11
15
  # - Automatic handling of nested data structures
16
+ # - Consistent handling of string-keyed API responses
12
17
  #
13
18
  # @example Basic usage
14
19
  # ```ruby
@@ -49,28 +54,48 @@ module PaystackSdk
49
54
  # @return [Hash, Array, Object] The underlying data
50
55
  attr_reader :raw_data
51
56
 
57
+ # @return [Integer] The status code of the API response
58
+ attr_reader :status_code
59
+
52
60
  # Initializes a new Response object
53
61
  #
54
62
  # @param response [Faraday::Response, Hash, Array] The raw API response or data
63
+ # @raise [PaystackSdk::APIError] If the API returns an error response
64
+ # @raise [PaystackSdk::AuthenticationError] If authentication fails (401)
65
+ # @raise [PaystackSdk::ResourceNotFoundError] If resource is not found (404 or 400 with not found message)
66
+ # @raise [PaystackSdk::RateLimitError] If rate limit is exceeded (429)
67
+ # @raise [PaystackSdk::ServerError] If server returns 5xx error
55
68
  def initialize(response)
56
69
  if response.is_a?(Faraday::Response)
57
- # Handle direct Faraday response
58
- @success = response.success?
70
+ @status_code = response.status
59
71
  @body = response.body
60
72
  @api_message = extract_api_message(@body)
61
73
  @raw_data = extract_data_from_body(@body)
62
74
 
63
- unless @success
64
- @error_message = @api_message || "Paystack API Error"
75
+ case @status_code
76
+ when 200..299
77
+ @success = true
78
+ when 400
79
+ handle_400_response
80
+ when 401
81
+ raise AuthenticationError.new
82
+ when 404
83
+ handle_404_response
84
+ when 429
85
+ retry_after = response.headers["Retry-After"]
86
+ raise RateLimitError.new(retry_after || 30)
87
+ when 500..599
88
+ raise ServerError.new(@status_code, @api_message)
89
+ else
90
+ @success = false
91
+ raise APIError.new(@api_message || "Paystack API Error")
65
92
  end
66
93
  elsif response.is_a?(Response)
67
- # If we're wrapping a Response object, just copy its data
68
- @success = response.success
94
+ @success = response.success?
69
95
  @error_message = response.error_message
70
96
  @api_message = response.api_message
71
97
  @raw_data = response.raw_data
72
98
  else
73
- # For direct data (Hash, Array, etc.)
74
99
  @success = true
75
100
  @raw_data = response
76
101
  end
@@ -197,24 +222,44 @@ module PaystackSdk
197
222
 
198
223
  private
199
224
 
200
- # Extract API message from response body
225
+ # Extract the identifier from an error response
226
+ # This looks for common patterns in error messages to find resource identifiers
201
227
  #
202
228
  # @param body [Hash] The response body
203
- # @return [String, nil] The API message
229
+ # @return [String] The extracted identifier or "unknown"
230
+ def extract_identifier(body)
231
+ return "unknown" unless body.is_a?(Hash)
232
+
233
+ # First try to get identifier from the message
234
+ message = body["message"].to_s.downcase
235
+ if message =~ /with (id|code|reference|email): ([^\s]+)/i
236
+ return $2
237
+ end
238
+
239
+ # If not found in message, try to extract from error code
240
+ if body["code"]&.match?(/^(transaction|customer)_/)
241
+ parts = body["code"].to_s.split("_")
242
+ return parts.last if parts.last != "not_found"
243
+ end
244
+
245
+ "unknown"
246
+ end
247
+
248
+ # Extract the API message from the response body
249
+ #
250
+ # @param body [Hash, nil] The response body
251
+ # @return [String, nil] The API message if present
204
252
  def extract_api_message(body)
205
- body["message"] || body[:message] if body.is_a?(Hash)
253
+ body["message"] if body.is_a?(Hash) && body["message"]
206
254
  end
207
255
 
208
- # Extract data from response body
256
+ # Extract the data from the response body
209
257
  #
210
- # @param body [Hash, Array] The response body
211
- # @return [Hash, Array, Object] The data portion of the response
258
+ # @param body [Hash, nil] The response body
259
+ # @return [Hash, Array, nil] The data from the response
212
260
  def extract_data_from_body(body)
213
- if body.is_a?(Hash)
214
- body["data"] || body[:data] || body
215
- else
216
- body
217
- end
261
+ return body unless body.is_a?(Hash)
262
+ body["data"] || body
218
263
  end
219
264
 
220
265
  # Wrap value in Response if needed
@@ -234,5 +279,29 @@ module PaystackSdk
234
279
  value
235
280
  end
236
281
  end
282
+
283
+ def handle_400_response
284
+ if @body["code"]&.end_with?("_not_found") ||
285
+ @api_message&.match?(/not found|cannot find|does not exist/i)
286
+ resource_type = determine_resource_type
287
+ meta_info = @body["meta"]&.dig("nextStep")
288
+ message = @api_message
289
+ message = "#{message}\nHint: #{meta_info}" if meta_info
290
+ raise ResourceNotFoundError.new(resource_type, message)
291
+ else
292
+ @success = false
293
+ raise APIError.new(@api_message || "Paystack API Error")
294
+ end
295
+ end
296
+
297
+ def handle_404_response
298
+ resource_type = determine_resource_type
299
+ raise ResourceNotFoundError.new(resource_type, @api_message)
300
+ end
301
+
302
+ def determine_resource_type
303
+ resource = @body["code"].split("_").first
304
+ resource.capitalize
305
+ end
237
306
  end
238
307
  end
@@ -23,7 +23,7 @@ module PaystackSdk
23
23
  else
24
24
  # Try to get API key from environment variable
25
25
  env_secret_key = ENV["PAYSTACK_SECRET_KEY"]
26
- raise PaystackSdk::Error, "No connection or API key provided" unless env_secret_key
26
+ raise AuthenticationError, "No connection or API key provided" unless env_secret_key
27
27
 
28
28
  create_connection(secret_key: env_secret_key)
29
29
  end
@@ -5,15 +5,41 @@ module PaystackSdk
5
5
  # It includes methods for validating common parameters like references, amounts, dates, etc.
6
6
  #
7
7
  # This module is intended to be included in resource classes to provide consistent
8
- # parameter validation before making API calls.
8
+ # parameter validation before making API calls. All validation methods raise specific
9
+ # error types to enable proper error handling in client applications.
10
+ #
11
+ # @example Usage in a resource class
12
+ # ```ruby
13
+ # class MyResource < PaystackSdk::Resources::Base
14
+ # include PaystackSdk::Validations
15
+ #
16
+ # def create(payload)
17
+ # validate_fields!(
18
+ # payload: payload,
19
+ # validations: {
20
+ # email: { type: :email, required: true },
21
+ # amount: { type: :positive_integer, required: true },
22
+ # currency: { type: :currency, required: true }
23
+ # }
24
+ # )
25
+ # # Make API call...
26
+ # end
27
+ # end
28
+ # ```
29
+ #
30
+ # @see PaystackSdk::MissingParamError
31
+ # @see PaystackSdk::InvalidFormatError
32
+ # @see PaystackSdk::InvalidValueError
9
33
  module Validations
10
34
  # Validates that input is a hash.
11
35
  #
12
36
  # @param input [Object] The input to validate
13
37
  # @param name [String] Name of the parameter for error messages
14
- # @raise [PaystackSdk::Error] If input is not a hash
38
+ # @raise [PaystackSdk::InvalidFormatError] If input is not a hash
15
39
  def validate_hash!(input:, name: "Payload")
16
- raise PaystackSdk::Error, "#{name} must be a hash" unless input.is_a?(Hash)
40
+ unless input.is_a?(Hash)
41
+ raise PaystackSdk::InvalidFormatError.new(name, "Hash")
42
+ end
17
43
  end
18
44
 
19
45
  # Validates that required parameters are present in a payload.
@@ -21,15 +47,15 @@ module PaystackSdk
21
47
  # @param payload [Hash] The payload to validate
22
48
  # @param required_params [Array<Symbol>] List of required parameter keys
23
49
  # @param operation_name [String] Name of the operation for error messages
24
- # @raise [PaystackSdk::Error] If any required parameters are missing
50
+ # @raise [PaystackSdk::MissingParamError] If any required parameters are missing
25
51
  def validate_required_params!(payload:, required_params:, operation_name: "Operation")
26
52
  missing_params = required_params.select do |param|
27
53
  !payload.key?(param) && !payload.key?(param.to_s)
28
54
  end
29
55
 
30
56
  unless missing_params.empty?
31
- missing_list = missing_params.map(&:to_s).join(", ")
32
- raise PaystackSdk::Error, "#{operation_name} requires these missing parameter(s): #{missing_list}"
57
+ param = missing_params.first
58
+ raise PaystackSdk::MissingParamError.new(param)
33
59
  end
34
60
  end
35
61
 
@@ -37,10 +63,10 @@ module PaystackSdk
37
63
  #
38
64
  # @param value [Object] The value to validate
39
65
  # @param name [String] Name of the parameter for error messages
40
- # @raise [PaystackSdk::Error] If value is nil or empty
66
+ # @raise [PaystackSdk::MissingParamError] If value is nil or empty
41
67
  def validate_presence!(value:, name: "Parameter")
42
68
  if value.nil? || (value.respond_to?(:empty?) && value.empty?)
43
- raise PaystackSdk::Error, "#{name} cannot be empty"
69
+ raise PaystackSdk::MissingParamError.new(name)
44
70
  end
45
71
  end
46
72
 
@@ -49,12 +75,13 @@ module PaystackSdk
49
75
  # @param value [Object] The value to validate
50
76
  # @param name [String] Name of the parameter for error messages
51
77
  # @param allow_nil [Boolean] Whether nil values are allowed
52
- # @raise [PaystackSdk::Error] If value is not a positive integer (and not nil if allow_nil is true)
78
+ # @raise [PaystackSdk::InvalidValueError] If value is not a positive integer
79
+ # @raise [PaystackSdk::MissingParamError] If value is nil and not allowed
53
80
  def validate_positive_integer!(value:, name: "Parameter", allow_nil: true)
54
81
  if value.nil?
55
- raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
82
+ raise PaystackSdk::MissingParamError.new(name) unless allow_nil
56
83
  elsif !value.is_a?(Integer) || value < 1
57
- raise PaystackSdk::Error, "#{name} must be a positive integer"
84
+ raise PaystackSdk::InvalidValueError.new(name, "must be a positive integer")
58
85
  end
59
86
  end
60
87
 
@@ -62,10 +89,10 @@ module PaystackSdk
62
89
  #
63
90
  # @param reference [String] The reference to validate
64
91
  # @param name [String] Name of the parameter for error messages
65
- # @raise [PaystackSdk::Error] If reference format is invalid
92
+ # @raise [PaystackSdk::InvalidFormatError] If reference format is invalid
66
93
  def validate_reference_format!(reference:, name: "Reference")
67
94
  unless reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/)
68
- raise PaystackSdk::Error, "#{name} can only contain alphanumeric characters and the following: -, ., ="
95
+ raise PaystackSdk::InvalidFormatError.new(name, "alphanumeric characters and the following: -, ., =")
69
96
  end
70
97
  end
71
98
 
@@ -74,17 +101,18 @@ module PaystackSdk
74
101
  # @param date_str [String] The date string to validate
75
102
  # @param name [String] Name of the parameter for error messages
76
103
  # @param allow_nil [Boolean] Whether nil values are allowed
77
- # @raise [PaystackSdk::Error] If date format is invalid
104
+ # @raise [PaystackSdk::InvalidFormatError] If date format is invalid
105
+ # @raise [PaystackSdk::MissingParamError] If date is nil and not allowed
78
106
  def validate_date_format!(date_str:, name: "Date", allow_nil: true)
79
107
  if date_str.nil?
80
- raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
108
+ raise PaystackSdk::MissingParamError.new(name) unless allow_nil
81
109
  return
82
110
  end
83
111
 
84
112
  begin
85
113
  Date.parse(date_str.to_s)
86
114
  rescue Date::Error
87
- raise PaystackSdk::Error, "Invalid #{name.downcase} format. Use format: YYYY-MM-DD or ISO8601"
115
+ raise PaystackSdk::InvalidFormatError.new(name, "YYYY-MM-DD or ISO8601")
88
116
  end
89
117
  end
90
118
 
@@ -94,20 +122,26 @@ module PaystackSdk
94
122
  # @param allowed_values [Array] List of allowed values
95
123
  # @param name [String] Name of the parameter for error messages
96
124
  # @param allow_nil [Boolean] Whether nil values are allowed
97
- # @param case_sensitive [Boolean] Whether the comparison is case-sensitive
98
- # @raise [PaystackSdk::Error] If value is not in the allowed set
99
- def validate_inclusion!(value:, allowed_values:, name: "Parameter", allow_nil: true, case_sensitive: false)
125
+ # @raise [PaystackSdk::InvalidValueError] If value is not in allowed_values
126
+ # @raise [PaystackSdk::MissingParamError] If value is nil and not allowed
127
+ #
128
+ # @example
129
+ # ```ruby
130
+ # validate_allowed_values!(
131
+ # value: "allow",
132
+ # allowed_values: %w[default allow deny],
133
+ # name: "risk_action"
134
+ # )
135
+ # ```
136
+ def validate_allowed_values!(value:, allowed_values:, name: "Parameter", allow_nil: true)
100
137
  if value.nil?
101
- raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
138
+ raise PaystackSdk::MissingParamError.new(name) unless allow_nil
102
139
  return
103
140
  end
104
141
 
105
- check_value = case_sensitive ? value.to_s : value.to_s.downcase
106
- check_allowed = case_sensitive ? allowed_values : allowed_values.map(&:downcase)
107
-
108
- unless check_allowed.include?(check_value)
109
- allowed_list = allowed_values.join("', '")
110
- raise PaystackSdk::Error, "#{name} must be one of: '#{allowed_list}'"
142
+ unless allowed_values.include?(value)
143
+ allowed_list = allowed_values.join(", ")
144
+ raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}")
111
145
  end
112
146
  end
113
147
 
@@ -119,12 +153,12 @@ module PaystackSdk
119
153
  # @raise [PaystackSdk::Error] If email format is invalid
120
154
  def validate_email!(email:, name: "Email", allow_nil: false)
121
155
  if email.nil?
122
- raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
156
+ raise PaystackSdk::MissingParamError.new(name) unless allow_nil
123
157
  return
124
158
  end
125
159
 
126
160
  unless email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
127
- raise PaystackSdk::Error, "Invalid #{name.downcase} format"
161
+ raise PaystackSdk::InvalidFormatError.new(name, "valid email address")
128
162
  end
129
163
  end
130
164
 
@@ -136,12 +170,12 @@ module PaystackSdk
136
170
  # @raise [PaystackSdk::Error] If currency format is invalid
137
171
  def validate_currency!(currency:, name: "Currency", allow_nil: true)
138
172
  if currency.nil?
139
- raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
173
+ raise PaystackSdk::MissingParamError.new(name) unless allow_nil
140
174
  return
141
175
  end
142
176
 
143
177
  unless currency.to_s.match?(/\A[A-Z]{3}\z/)
144
- raise PaystackSdk::Error, "#{name} must be a 3-letter ISO code (e.g., NGN, USD, GHS)"
178
+ raise PaystackSdk::InvalidFormatError.new(name, "3-letter ISO code (e.g., NGN, USD, GHS)")
145
179
  end
146
180
  end
147
181
 
@@ -152,6 +186,7 @@ module PaystackSdk
152
186
  # @raise [PaystackSdk::Error] If any validation fails
153
187
  #
154
188
  # @example
189
+ # ```ruby
155
190
  # validate_fields!(
156
191
  # payload: params,
157
192
  # validations: {
@@ -161,7 +196,10 @@ module PaystackSdk
161
196
  # reference: { type: :reference, required: false }
162
197
  # }
163
198
  # )
199
+ # ```
164
200
  def validate_fields!(payload:, validations:)
201
+ validate_hash!(input: payload, name: "Payload")
202
+
165
203
  # First check required fields
166
204
  required_fields = validations.select { |_, opts| opts[:required] }.keys
167
205
  validate_required_params!(payload: payload, required_params: required_fields) unless required_fields.empty?
@@ -184,14 +222,15 @@ module PaystackSdk
184
222
  validate_currency!(currency: value, name: field.to_s, allow_nil: !options[:required])
185
223
  when :inclusion
186
224
  if value
187
- validate_inclusion!(
225
+ validate_allowed_values!(
188
226
  value: value,
189
227
  allowed_values: options[:allowed_values],
190
228
  name: field.to_s,
191
- allow_nil: !options[:required],
192
- case_sensitive: options[:case_sensitive]
229
+ allow_nil: !options[:required]
193
230
  )
194
231
  end
232
+ when :string
233
+ validate_presence!(value: value, name: field.to_s) if !options[:allow_nil] && options[:required]
195
234
  end
196
235
  end
197
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaystackSdk
4
- VERSION = "0.0.5"
4
+ VERSION = "0.0.6"
5
5
  end
data/lib/paystack_sdk.rb CHANGED
@@ -5,5 +5,87 @@ require_relative "paystack_sdk/version"
5
5
  require_relative "paystack_sdk/client"
6
6
 
7
7
  module PaystackSdk
8
+ # Base error class for all Paystack SDK errors.
9
+ # All SDK-specific exceptions inherit from this class.
8
10
  class Error < StandardError; end
11
+
12
+ # Base class for all validation errors.
13
+ # Raised when input parameters fail validation before API calls.
14
+ class ValidationError < Error; end
15
+
16
+ # Raised when a required parameter is missing.
17
+ # Contains the parameter name for detailed error handling.
18
+ class MissingParamError < ValidationError
19
+ attr_reader :param_name
20
+
21
+ def initialize(param_name)
22
+ @param_name = param_name
23
+ super("Missing required parameter: #{param_name}")
24
+ end
25
+ end
26
+
27
+ # Raised when a parameter has an invalid format.
28
+ # Contains both the parameter name and expected format for detailed error handling.
29
+ class InvalidFormatError < ValidationError
30
+ attr_reader :param_name, :expected_format
31
+
32
+ def initialize(param_name, expected_format)
33
+ @param_name = param_name
34
+ @expected_format = expected_format
35
+ super("Invalid format for #{param_name}. Expected format: #{expected_format}")
36
+ end
37
+ end
38
+
39
+ # Raised when a parameter has an invalid value.
40
+ # Contains both the parameter name and the reason for the invalid value.
41
+ class InvalidValueError < ValidationError
42
+ attr_reader :param_name, :reason
43
+
44
+ def initialize(param_name, reason)
45
+ @param_name = param_name
46
+ @reason = reason
47
+ super("Invalid value for #{param_name}: #{reason}")
48
+ end
49
+ end
50
+
51
+ # Base class for API errors.
52
+ # Raised when the Paystack API returns an error response.
53
+ class APIError < Error; end
54
+
55
+ # Raised when authentication fails
56
+ class AuthenticationError < APIError
57
+ def initialize(message = "Invalid API key or authentication failed")
58
+ super
59
+ end
60
+ end
61
+
62
+ # Raised when a resource is not found
63
+ class ResourceNotFoundError < APIError
64
+ attr_reader :resource_type
65
+
66
+ def initialize(resource_type, message)
67
+ @resource_type = resource_type
68
+ super(message)
69
+ end
70
+ end
71
+
72
+ # Raised when rate limiting is encountered
73
+ class RateLimitError < APIError
74
+ attr_reader :retry_after
75
+
76
+ def initialize(retry_after)
77
+ @retry_after = retry_after
78
+ super("Rate limit exceeded. Retry after #{retry_after} seconds")
79
+ end
80
+ end
81
+
82
+ # Raised when the server returns a 5xx error
83
+ class ServerError < APIError
84
+ attr_reader :status_code
85
+
86
+ def initialize(status_code, message = "An error occurred on the Paystack server")
87
+ @status_code = status_code
88
+ super("#{message} (Status: #{status_code})")
89
+ end
90
+ end
9
91
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paystack_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxwell Nana Forson (theLazyProgrammer)
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-05-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -117,6 +116,7 @@ files:
117
116
  - lib/paystack_sdk.rb
118
117
  - lib/paystack_sdk/client.rb
119
118
  - lib/paystack_sdk/resources/base.rb
119
+ - lib/paystack_sdk/resources/customers.rb
120
120
  - lib/paystack_sdk/resources/transactions.rb
121
121
  - lib/paystack_sdk/response.rb
122
122
  - lib/paystack_sdk/utils/connection_utils.rb
@@ -131,7 +131,6 @@ metadata:
131
131
  allowed_push_host: https://rubygems.org
132
132
  homepage_uri: https://github.com/nanafox/paystack_sdk
133
133
  changelog_uri: https://github.com/nanafox/paystack_sdk/blob/main/CHANGELOG.md
134
- post_install_message:
135
134
  rdoc_options: []
136
135
  require_paths:
137
136
  - lib
@@ -146,8 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
145
  - !ruby/object:Gem::Version
147
146
  version: '0'
148
147
  requirements: []
149
- rubygems_version: 3.5.16
150
- signing_key:
148
+ rubygems_version: 3.6.9
151
149
  specification_version: 4
152
150
  summary: A Ruby SDK for integrating with Paystack's payment gateway API.
153
151
  test_files: []