paystack_sdk 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1161ca83326adeace91da47a17f4c078cee7f6073b4efda0d0d0e707b9744928
4
- data.tar.gz: 44a2b6e56897e8e3f020705f2b836f6a6172599f43b1c976144a2cab9165616d
3
+ metadata.gz: a18abe45774bda30eff6f91a3e93ba55066d9ef4f04ddfc690792edc8b005d94
4
+ data.tar.gz: 652cc2b936ac3a7211290ad6a6f561d57132fd825588232fab724a0b2378cfa7
5
5
  SHA512:
6
- metadata.gz: 59fc459f0ca4b103c4815c6523d6a6f6b7bf8088ef391c3d980b829f512aab9e66081796e2713d93d09cfeadb73c6069101ca91ce3b993ccf47389e0d8404df4
7
- data.tar.gz: c4cc838ad71d8a8860b037096db5cf3c8cc55ed73a735eb405b4e81bd065a935de1529aa7e31876c64901c3f8fe973c9b2e7294924c7fd36881668a14297056a
6
+ metadata.gz: 5fcb8324376b91af1c71e894309789fb69a4eeefff5d4a1fa93c190a66c9c01489d5a8a9b4003f8416fbff3ffe5d3474ec6ec49ab9c1a58ed7fb16ebf095c04a
7
+ data.tar.gz: b094c91f15744f8c02ca9e493d7aa6ab7b9c24f6f0e25cc8e1a94d32bc9d3101aa713a97ff074000e365e1f1857d26735bd99016e82a480b7da839e6be9b349e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.0] - 2025-06-26
4
+
5
+ ### Changed
6
+
7
+ #### Error Handling Strategy
8
+
9
+ - **BREAKING**: Clarified error handling approach - the SDK now consistently follows a two-tier error handling strategy:
10
+ - **Validation errors** (missing/invalid input data) are thrown as exceptions immediately before API calls
11
+ - **API response errors** (business logic, not found, etc.) are returned as unsuccessful Response objects
12
+ - Updated documentation to clearly explain when exceptions are thrown vs. when to check `response.success?`
13
+
14
+ #### Test Infrastructure
15
+
16
+ - **BREAKING**: Standardized all test specifications to use connection doubles instead of Faraday-specific mocks
17
+ - Updated test pattern across all resource specs to use `instance_double("PaystackSdk::Connection")` for better framework independence
18
+ - All tests now follow consistent mocking pattern with `PaystackSdk::Response.new()` expectations
19
+
20
+ #### Documentation
21
+
22
+ - Enhanced README with comprehensive error handling examples
23
+ - Added validation error examples showing `MissingParamError`, `InvalidFormatError`, and `InvalidValueError`
24
+ - Updated Quick Start guide to demonstrate proper exception handling
25
+ - Clarified the difference between input validation (exceptions) and API response handling (Response objects)
26
+
27
+ ### Improved
28
+
29
+ - Better separation of concerns between input validation and API error handling
30
+ - More consistent test suite that's not tied to specific HTTP client implementation
31
+ - Clearer developer experience with predictable error handling patterns
32
+
33
+ ## [0.0.9] - 2025-06-26
34
+
35
+ ### Added
36
+
37
+ - Verification resource with support for:
38
+ - Resolving bank accounts
39
+ - Resolving card BINs
40
+ - Validating accounts (with required and optional fields)
41
+ - Registered `verification` resource in the main client for easy access
42
+ - Regression tests for all Verification resource methods, including required field validation and full parameter support
43
+
44
+ ### Changed
45
+
46
+ - Fix the link for listing banks API
47
+
3
48
  ## [0.0.8] - 2025-06-26
4
49
 
5
50
  ### Added
data/README.md CHANGED
@@ -69,13 +69,19 @@ params = {
69
69
  currency: "NGN"
70
70
  }
71
71
 
72
- response = paystack.transactions.initiate(params)
72
+ begin
73
+ response = paystack.transactions.initiate(params)
73
74
 
74
- if response.success?
75
- puts "Visit this URL to complete payment: #{response.authorization_url}"
76
- puts "Transaction reference: #{response.reference}"
77
- else
78
- puts "Error: #{response.error_message}"
75
+ if response.success?
76
+ puts "Visit this URL to complete payment: #{response.authorization_url}"
77
+ puts "Transaction reference: #{response.reference}"
78
+ else
79
+ puts "Error: #{response.error_message}"
80
+ end
81
+ rescue PaystackSdk::MissingParamError => e
82
+ puts "Missing required data: #{e.message}"
83
+ rescue PaystackSdk::InvalidFormatError => e
84
+ puts "Invalid data format: #{e.message}"
79
85
  end
80
86
 
81
87
  # Create a customer
@@ -85,12 +91,16 @@ customer_params = {
85
91
  last_name: "Doe"
86
92
  }
87
93
 
88
- customer_response = paystack.customers.create(customer_params)
94
+ begin
95
+ customer_response = paystack.customers.create(customer_params)
89
96
 
90
- if customer_response.success?
91
- puts "Customer created: #{customer_response.data.customer_code}"
92
- else
93
- puts "Error: #{customer_response.error_message}"
97
+ if customer_response.success?
98
+ puts "Customer created: #{customer_response.data.customer_code}"
99
+ else
100
+ puts "Error: #{customer_response.error_message}"
101
+ end
102
+ rescue PaystackSdk::ValidationError => e
103
+ puts "Validation error: #{e.message}"
94
104
  end
95
105
  ```
96
106
 
@@ -98,6 +108,55 @@ end
98
108
 
99
109
  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
110
 
111
+ ### Error Handling
112
+
113
+ The SDK uses a two-tier error handling approach:
114
+
115
+ 1. **Validation Errors** (thrown as exceptions) - for missing or invalid input data
116
+ 2. **API Response Errors** (returned as unsuccessful Response objects) - for API-level issues
117
+
118
+ #### Input Validation
119
+
120
+ The SDK validates your parameters **before** making API calls and throws exceptions immediately for data issues:
121
+
122
+ ```ruby
123
+ begin
124
+ # This will throw an exception before making any API call
125
+ response = paystack.transactions.initiate({amount: 1000}) # Missing required email
126
+ rescue PaystackSdk::MissingParamError => e
127
+ puts "Fix your data: #{e.message}"
128
+ end
129
+ ```
130
+
131
+ #### API Response Handling
132
+
133
+ All successful API calls return a `Response` object that you can check for success:
134
+
135
+ ```ruby
136
+ response = paystack.transactions.initiate(valid_params)
137
+
138
+ if response.success?
139
+ puts "Transaction created: #{response.authorization_url}"
140
+ else
141
+ puts "Transaction failed: #{response.error_message}"
142
+
143
+ # Get detailed error information
144
+ error_details = response.error_details
145
+ puts "Status code: #{error_details[:status_code]}"
146
+ puts "Error message: #{error_details[:message]}"
147
+ end
148
+ ```
149
+
150
+ **Note**: The SDK raises exceptions for:
151
+
152
+ - **Validation errors** - when required parameters are missing or have invalid formats
153
+ - **Authentication errors** (401) - usually configuration issues
154
+ - **Rate limiting** (429) - requires retry logic
155
+ - **Server errors** (5xx) - Paystack infrastructure issues
156
+ - **Network errors** - connection failures
157
+
158
+ All other API errors (resource not found, business logic errors, etc.) are returned as unsuccessful Response objects.
159
+
101
160
  ## Usage
102
161
 
103
162
  ### Client Initialization
@@ -457,19 +516,19 @@ total_count = original.dig("meta", "total")
457
516
  current_page = original.dig("meta", "page")
458
517
  ```
459
518
 
460
- #### Error Handling
519
+ #### Exception Handling
461
520
 
462
521
  The SDK provides specific error classes for different types of failures, making it easier to handle errors appropriately:
463
522
 
464
523
  ```ruby
465
524
  begin
466
525
  response = paystack.transactions.verify(reference: "invalid_reference")
467
- rescue PaystackSdk::ResourceNotFoundError => e
468
- puts "Resource not found: #{e.message}"
469
526
  rescue PaystackSdk::AuthenticationError => e
470
527
  puts "Authentication failed: #{e.message}"
471
- rescue PaystackSdk::ValidationError => e
472
- puts "Validation error: #{e.message}"
528
+ rescue PaystackSdk::RateLimitError => e
529
+ puts "Rate limit exceeded. Retry after: #{e.retry_after} seconds"
530
+ rescue PaystackSdk::ServerError => e
531
+ puts "Server error: #{e.message}"
473
532
  rescue PaystackSdk::APIError => e
474
533
  puts "API error: #{e.message}"
475
534
  rescue PaystackSdk::Error => e
@@ -507,6 +566,41 @@ The SDK includes several specific error classes:
507
566
  - **`PaystackSdk::RateLimitError`** - Rate limiting encountered
508
567
  - **`PaystackSdk::ServerError`** - Server errors (5xx responses)
509
568
 
569
+ ##### Validation Error Examples
570
+
571
+ The SDK validates your input data **before** making API calls and will throw exceptions immediately if required data is missing or incorrectly formatted:
572
+
573
+ ```ruby
574
+ # Missing required parameter
575
+ begin
576
+ paystack.transactions.initiate({amount: 1000}) # Missing email
577
+ rescue PaystackSdk::MissingParamError => e
578
+ puts e.message # => "Missing required parameter: email"
579
+ end
580
+
581
+ # Invalid format
582
+ begin
583
+ paystack.transactions.initiate({
584
+ email: "invalid-email", # Not a valid email format
585
+ amount: 1000
586
+ })
587
+ rescue PaystackSdk::InvalidFormatError => e
588
+ puts e.message # => "Invalid format for Email. Expected format: valid email address"
589
+ end
590
+
591
+ # Invalid value
592
+ begin
593
+ paystack.customers.set_risk_action({
594
+ customer: "CUS_123",
595
+ risk_action: "invalid_action" # Not in allowed values
596
+ })
597
+ rescue PaystackSdk::InvalidValueError => e
598
+ puts e.message # => "Invalid value for risk_action: must be one of [default, allow, deny]"
599
+ end
600
+ ```
601
+
602
+ These validation errors are thrown immediately and prevent the API call from being made, helping you catch data issues early in development.
603
+
510
604
  ## Advanced Usage
511
605
 
512
606
  ### Environment Variables
@@ -5,6 +5,7 @@ require_relative "resources/customers"
5
5
  require_relative "resources/transfer_recipients"
6
6
  require_relative "resources/transfers"
7
7
  require_relative "resources/banks"
8
+ require_relative "resources/verification"
8
9
  require_relative "utils/connection_utils"
9
10
 
10
11
  module PaystackSdk
@@ -106,5 +107,19 @@ module PaystackSdk
106
107
  def banks
107
108
  @banks ||= Resources::Banks.new(@connection)
108
109
  end
110
+
111
+ # Provides access to the `Verification` resource.
112
+ #
113
+ # @return [PaystackSdk::Resources::Verification] An instance of the
114
+ # `Verification` resource.
115
+ #
116
+ # @example
117
+ # ```ruby
118
+ # verification = client.verification
119
+ # response = verification.resolve_account(account_number: ..., bank_code: ...)
120
+ # ```
121
+ def verification
122
+ @verification ||= Resources::Verification.new(@connection)
123
+ end
109
124
  end
110
125
  end
@@ -4,7 +4,7 @@ module PaystackSdk
4
4
  module Resources
5
5
  class Banks < Base
6
6
  # List banks
7
- # @see https://paystack.com/docs/api/bank/#list
7
+ # @see https://paystack.com/docs/api/miscellaneous/#bank
8
8
  def list(query = {})
9
9
  if query.key?(:currency)
10
10
  validate_allowed_values!(
@@ -0,0 +1,36 @@
1
+ require_relative "../validations"
2
+ require_relative "base"
3
+
4
+ module PaystackSdk
5
+ module Resources
6
+ class Verification < Base
7
+ # Resolve Bank Account
8
+ # @see https://paystack.com/docs/api/verification/#resolve-bank-account
9
+ def resolve_account(account_number:, bank_code:)
10
+ validate_presence!(value: account_number, name: "account_number")
11
+ validate_presence!(value: bank_code, name: "bank_code")
12
+ handle_response(@connection.get("/bank/resolve", {account_number: account_number, bank_code: bank_code}))
13
+ end
14
+
15
+ # Resolve Card BIN
16
+ # @see https://paystack.com/docs/api/verification/#resolve-card-bin
17
+ def resolve_card_bin(bin)
18
+ validate_presence!(value: bin, name: "bin")
19
+ handle_response(@connection.get("/decision/bin/#{bin}"))
20
+ end
21
+
22
+ # Validate Account
23
+ # @see https://paystack.com/docs/api/verification/#validate-account
24
+ # Required: account_number, account_name, account_type, bank_code, country_code, document_type
25
+ # Optional: document_number
26
+ def validate_account(params)
27
+ validate_required_params!(
28
+ payload: params,
29
+ required_params: %i[account_number account_name account_type bank_code country_code document_type],
30
+ operation_name: "Validate Account"
31
+ )
32
+ handle_response(@connection.post("/bank/validate", params))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -63,11 +63,11 @@ module PaystackSdk
63
63
  # Initializes a new Response object
64
64
  #
65
65
  # @param response [Faraday::Response, Hash, Array] The raw API response or data
66
- # @raise [PaystackSdk::APIError] If the API returns an error response
67
66
  # @raise [PaystackSdk::AuthenticationError] If authentication fails (401)
68
- # @raise [PaystackSdk::ResourceNotFoundError] If resource is not found (404 or 400 with not found message)
69
67
  # @raise [PaystackSdk::RateLimitError] If rate limit is exceeded (429)
70
68
  # @raise [PaystackSdk::ServerError] If server returns 5xx error
69
+ # @note API-level errors (4xx except 401) are returned as unsuccessful Response objects
70
+ # rather than raised as exceptions. Use response.success? to check for errors.
71
71
  def initialize(response)
72
72
  if response.is_a?(Faraday::Response)
73
73
  @status_code = response.status
@@ -79,20 +79,25 @@ module PaystackSdk
79
79
  case @status_code
80
80
  when 200..299
81
81
  @success = true
82
- when 400
83
- handle_400_response
84
- when 401
85
- raise AuthenticationError.new
86
- when 404
87
- handle_404_response
82
+ when 400..499
83
+ # Client errors - return unsuccessful response for user to handle
84
+ @success = false
85
+ @error_message = @api_message || "Client error"
86
+
87
+ # Still raise for authentication issues as these are usually config problems
88
+ if @status_code == 401
89
+ raise AuthenticationError.new(@api_message || "Authentication failed")
90
+ end
88
91
  when 429
92
+ # Rate limiting - raise as users need to implement retry logic
89
93
  retry_after = response.headers["Retry-After"]
90
94
  raise RateLimitError.new(retry_after || 30)
91
95
  when 500..599
96
+ # Server errors - raise as these indicate Paystack infrastructure issues
92
97
  raise ServerError.new(@status_code, @api_message)
93
98
  else
94
99
  @success = false
95
- raise APIError.new(@api_message || "Paystack API Error")
100
+ @error_message = @api_message || "Unknown error"
96
101
  end
97
102
  elsif response.is_a?(Response)
98
103
  @success = response.success?
@@ -120,6 +125,26 @@ module PaystackSdk
120
125
  @success
121
126
  end
122
127
 
128
+ # Check if the response failed
129
+ #
130
+ # @return [Boolean] true if the API request failed
131
+ def failed?
132
+ !@success
133
+ end
134
+
135
+ # Get error information if the request failed
136
+ #
137
+ # @return [Hash] Hash containing error details, or empty hash if successful
138
+ def error_details
139
+ return {} if success?
140
+
141
+ {
142
+ status_code: @status_code,
143
+ message: @error_message || @api_message,
144
+ raw_response: @body
145
+ }.compact
146
+ end
147
+
123
148
  # Returns the original response body
124
149
  # This is useful for debugging or accessing raw data
125
150
  #
@@ -284,28 +309,15 @@ module PaystackSdk
284
309
  end
285
310
  end
286
311
 
287
- def handle_400_response
288
- if @body["code"]&.end_with?("_not_found") ||
289
- @api_message&.match?(/not found|cannot find|does not exist/i)
290
- resource_type = determine_resource_type
291
- meta_info = @body["meta"]&.dig("nextStep")
292
- message = @api_message
293
- message = "#{message}\nHint: #{meta_info}" if meta_info
294
- raise ResourceNotFoundError.new(resource_type, message)
295
- else
296
- @success = false
297
- raise APIError.new(@api_message || "Paystack API Error")
298
- end
299
- end
312
+ def determine_resource_type
313
+ return "Unknown" unless @body.is_a?(Hash) && @body["code"]
300
314
 
301
- def handle_404_response
302
- resource_type = determine_resource_type
303
- raise ResourceNotFoundError.new(resource_type, @api_message)
304
- end
315
+ if @body["code"].include?("_")
316
+ parts = @body["code"].to_s.split("_")
317
+ return parts.last if parts.last != "not_found"
318
+ end
305
319
 
306
- def determine_resource_type
307
- resource = @body["code"].split("_").first
308
- resource.capitalize
320
+ "unknown"
309
321
  end
310
322
  end
311
323
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaystackSdk
4
- VERSION = "0.0.8"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paystack_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxwell Nana Forson (theLazyProgrammer)
@@ -121,6 +121,7 @@ files:
121
121
  - lib/paystack_sdk/resources/transactions.rb
122
122
  - lib/paystack_sdk/resources/transfer_recipients.rb
123
123
  - lib/paystack_sdk/resources/transfers.rb
124
+ - lib/paystack_sdk/resources/verification.rb
124
125
  - lib/paystack_sdk/response.rb
125
126
  - lib/paystack_sdk/utils/connection_utils.rb
126
127
  - lib/paystack_sdk/validations.rb