paystack_sdk 0.0.3 → 0.0.5

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: cfe0137a6cc918043bc34e2be6dcb579713ef3dfec0641763234e66d51e08717
4
- data.tar.gz: a331be86e1c23da3621d4b6a1d6d6c5ecfa31de7685c3776872b3ff6f3adc99d
3
+ metadata.gz: 5dc8af5f5a8c1305ac52bb936f3601982204a1ccab20ee7d63b2b7ad89c03b3f
4
+ data.tar.gz: 30c6c8e89a107566d079ca22f78f56cbb6d48701db2d97f60ddb6357a2f5c5b7
5
5
  SHA512:
6
- metadata.gz: de4c777e2d89ce4cf1e30a13a352d4dab8bee5696716af0eb771964ce3aad3a10b0a6381c67b64c42dea342954c5e6d69d05b75fa43e72c288f55eeb45192a7d
7
- data.tar.gz: cc889820541066ea486dfe9b4aa86d1009c692efd7c1fa52929a0791ae1f954ecabd0c66bb75b733bae0af44398e6687004867a78dc64bf7f33b33ec81eaebbc
6
+ metadata.gz: b5f448b8d57b73b3837542791f56418b1711dc697b7551b1240eac3b86d5337d7a125b70d006654ef41a84e16b40da38ad5bb5178faf50fee12ac72ca9d8c7f1
7
+ data.tar.gz: 6c3d0d9eb5c06106e1fea26e7434034a42693925cae29a7cbf9288d5655c48598a91a80cc353b238dfdd9bb73be957d4f2e511fc54004260521aa8fedaf17cf3
data/.standard.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
3
  ruby_version: 3.2.2
4
+ fix: true
5
+ format: progress
data/README.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  The `paystack_sdk` gem provides a simple and intuitive interface for interacting with Paystack's payment gateway API. It allows developers to easily integrate Paystack's payment processing features into their Ruby applications. With support for various endpoints, this SDK simplifies tasks such as initiating transactions, verifying payments, managing customers, and more.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [Usage](#usage)
10
+ - [Client Initialization](#client-initialization)
11
+ - [Transactions](#transactions)
12
+ - [Initialize a Transaction](#initialize-a-transaction)
13
+ - [Verify a Transaction](#verify-a-transaction)
14
+ - [List Transactions](#list-transactions)
15
+ - [Fetch a Transaction](#fetch-a-transaction)
16
+ - [Get Transaction Totals](#get-transaction-totals)
17
+ - [Response Handling](#response-handling)
18
+ - [Working with Response Objects](#working-with-response-objects)
19
+ - [Accessing the Original Response](#accessing-the-original-response)
20
+ - [Error Handling](#error-handling)
21
+ - [Advanced Usage](#advanced-usage)
22
+ - [Environment Variables](#environment-variables)
23
+ - [Direct Resource Instantiation](#direct-resource-instantiation)
24
+ - [Development](#development)
25
+ - [Contributing](#contributing)
26
+ - [License](#license)
27
+ - [Code of Conduct](#code-of-conduct)
28
+
5
29
  ## Installation
6
30
 
7
31
  Add this line to your application's Gemfile:
@@ -22,52 +46,284 @@ Or install it yourself as:
22
46
  gem install paystack_sdk
23
47
  ```
24
48
 
49
+ ## Quick Start
50
+
51
+ ```ruby
52
+ require 'paystack_sdk'
53
+
54
+ # Initialize the client with your secret key
55
+ paystack = PaystackSdk::Client.new(secret_key: "sk_test_xxx")
56
+
57
+ # Initialize a transaction
58
+ params = {
59
+ email: "customer@email.com",
60
+ amount: 2300, # Amount in the smallest currency unit (kobo for NGN)
61
+ currency: "NGN"
62
+ }
63
+
64
+ response = paystack.transactions.initiate(params)
65
+
66
+ if response.success?
67
+ puts "Visit this URL to complete payment: #{response.authorization_url}"
68
+ else
69
+ puts "Error: #{response.error_message}"
70
+ end
71
+ ```
72
+
25
73
  ## Usage
26
74
 
27
- Here’s a basic example of how to use the `paystack_sdk` gem:
75
+ ### Client Initialization
28
76
 
29
77
  ```ruby
30
- require 'paystack_sdk'
78
+ # Initialize with your Paystack secret key
79
+ paystack = PaystackSdk::Client.new(secret_key: "sk_test_xxx")
80
+
81
+ # Or set the PAYSTACK_SECRET_KEY in your environment and do this instead
82
+ paystack = PaystackSdk::Client.new # => This will dynamically fetch the secret key
83
+
84
+ # You can access the connection directly if needed
85
+ connection = paystack.connection
86
+ ```
31
87
 
32
- # Initialize the SDK with your Paystack secret key
33
- paystack = PaystackSdk::Client.new(api_key: "sk_test_xxx")
88
+ ### Transactions
34
89
 
35
- # Example: Initialize a payment
36
- params = {email: "customer@email.com", amount: "2300", currency: "USD"}
37
- response = paystack.transactions.initialize_transaction(params)
90
+ The SDK provides comprehensive support for Paystack's Transaction API.
91
+
92
+ #### Initialize a Transaction
93
+
94
+ ```ruby
95
+ # Prepare transaction parameters
96
+ params = {
97
+ email: "customer@example.com",
98
+ amount: 10000, # Amount in the smallest currency unit (e.g., kobo, pesewas, cents)
99
+ currency: "GHS",
100
+ callback_url: "https://example.com/callback"
101
+ }
102
+
103
+ # Initialize the transaction
104
+ response = paystack.transactions.initiate(params)
38
105
 
39
106
  if response.success?
40
- puts response.authorization_url
107
+ puts "Transaction initialized successfully!"
108
+ puts "Authorization URL: #{response.authorization_url}"
109
+ puts "Access Code: #{response.access_code}"
110
+ puts "Reference: #{response.reference}"
41
111
  else
42
- puts resposne.error_message
112
+ puts "Error: #{response.error_message}"
43
113
  end
114
+ ```
44
115
 
45
- # Example: Verify a payment
116
+ #### Verify a Transaction
117
+
118
+ ```ruby
119
+ # Verify using transaction reference
46
120
  response = paystack.transactions.verify(reference: "transaction_reference")
121
+
122
+ if response.success?
123
+ transaction = response.data
124
+ puts "Transaction verified successfully!"
125
+ puts "Status: #{transaction.status}"
126
+ puts "Amount: #{transaction.amount}"
127
+ puts "Currency: #{transaction.currency}"
128
+ puts "Customer Email: #{transaction.customer.email}"
129
+
130
+ # Check specific transaction status
131
+ case transaction.status
132
+ when "success"
133
+ puts "Payment successful!"
134
+ when "pending"
135
+ puts "Payment is pending."
136
+ else
137
+ puts "Current status: #{transaction.status}"
138
+ end
139
+ else
140
+ puts "Verification failed: #{response.error_message}"
141
+ end
142
+ ```
143
+
144
+ #### List Transactions
145
+
146
+ ```ruby
147
+ # Get all transactions (default pagination: 50 per page)
148
+ response = paystack.transactions.list
149
+
150
+ # With custom pagination
151
+ response = paystack.transactions.list(per_page: 20, page: 2)
152
+
153
+ # With additional filters
154
+ response = paystack.transactions.list(
155
+ per_page: 10,
156
+ page: 1,
157
+ from: "2025-01-01",
158
+ to: "2025-04-30",
159
+ status: "success"
160
+ )
161
+
47
162
  if response.success?
48
- puts "Payment verified successfully!"
163
+ puts "Total transactions: #{response.count}" # response.size is another way
164
+
165
+ response.data.each do |transaction|
166
+ puts "ID: #{transaction.id}"
167
+ puts "Reference: #{transaction.reference}"
168
+ puts "Amount: #{transaction.amount}"
169
+ puts "----------------"
170
+ end
171
+
172
+ # Get the first transaction
173
+ first_transaction = response.data.first
174
+ puts "First transaction reference: #{first_transaction.reference}"
175
+
176
+ # Get the last transaction
177
+ last_transaction = response.data.last
178
+ puts "Last transaction amount: #{last_transaction.amount}"
49
179
  else
50
- puts "Payment verification failed: #{response.error_message}"
180
+ puts "Error: #{response.error_message}"
51
181
  end
52
182
  ```
53
183
 
54
- ### The Orginal Response
184
+ #### Fetch a Transaction
55
185
 
56
- There will be times you may need access to the original API response. For such cases, you
57
- can use the `#original_response` method on the response object.
186
+ ```ruby
187
+ # Fetch a specific transaction by ID
188
+ transaction_id = "12345"
189
+ response = paystack.transactions.fetch(transaction_id)
58
190
 
59
- The return value is a hash with all the values from the HTTP request. This could be useful
60
- when you need to debug or gain access to the response its raw state.
191
+ if response.success?
192
+ transaction = response.data
193
+ puts "Transaction details:"
194
+ puts "ID: #{transaction.id}"
195
+ puts "Reference: #{transaction.reference}"
196
+ puts "Amount: #{transaction.amount}"
197
+ puts "Status: #{transaction.status}"
198
+
199
+ # Access customer information
200
+ puts "Customer Email: #{transaction.customer.email}"
201
+ puts "Customer Name: #{transaction.customer.name}"
202
+ else
203
+ puts "Error: #{response.error_message}"
204
+ end
205
+ ```
206
+
207
+ #### Get Transaction Totals
61
208
 
62
- For example
63
209
  ```ruby
64
- response = transaction.list
210
+ # Get transaction volume and success metrics
211
+ response = paystack.transactions.totals
212
+
213
+ if response.success?
214
+ puts "Total Transactions: #{response.data.total_transactions}"
215
+ puts "Total Volume: #{response.data.total_volume}"
216
+ puts "Pending Transfers: #{response.data.pending_transfers}"
217
+ else
218
+ puts "Error: #{response.error_message}"
219
+ end
220
+ ```
221
+
222
+ ### Response Handling
223
+
224
+ #### Working with Response Objects
225
+
226
+ All API requests return a `PaystackSdk::Response` object that provides easy access to the response data.
227
+
228
+ ```ruby
229
+ response = paystack.transactions.initiate(params)
230
+
231
+ # Check if the request was successful
232
+ response.success? # => true or false
233
+
234
+ # Access response message
235
+ response.api_message # => "Authorization URL created"
236
+
237
+ # Access data using dot notation
238
+ response.data.authorization_url
239
+ response.data.access_code
240
+
241
+ # Access data directly from the response
242
+ response.authorization_url # Same as response.data.authorization_url
243
+
244
+ # Access nested data
245
+ response.data.customer.email
246
+
247
+ # For arrays, use array methods
248
+ response.data.first # First item in an array
249
+ response.data.last # Last item in an array
250
+ response.data.size # Size of the array
251
+
252
+ # Iterate through array data
253
+ response.data.each do |item|
254
+ puts item.id
255
+ end
256
+ ```
257
+
258
+ #### Accessing the Original Response
259
+
260
+ Sometimes you may need access to the original API response:
261
+
262
+ ```ruby
263
+ response = paystack.transactions.list
264
+
265
+ # Access the original response body
266
+ original = response.original_response
267
+
268
+ # Access metadata from the original response
269
+ total_count = original.dig("meta", "total")
270
+ current_page = original.dig("meta", "page")
271
+ ```
272
+
273
+ #### Error Handling
274
+
275
+ ```ruby
276
+ response = paystack.transactions.verify(reference: "invalid_reference")
277
+
278
+ unless response.success?
279
+ puts "Error: #{response.error_message}"
280
+
281
+ # Take action based on the error
282
+ if response.error_message.include?("not found")
283
+ puts "The transaction reference was not found."
284
+ elsif response.error_message.include?("Invalid key")
285
+ puts "API authentication failed. Check your API key."
286
+ end
287
+ end
288
+ ```
289
+
290
+ ## Advanced Usage
291
+
292
+ ### Environment Variables
293
+
294
+ You can use environment variables to configure the SDK:
295
+
296
+ ```ruby
297
+ # Set the PAYSTACK_SECRET_KEY environment variable
298
+ ENV["PAYSTACK_SECRET_KEY"] = "sk_test_xxx"
299
+
300
+ # Then initialize resources without specifying the key
301
+ transactions = PaystackSdk::Resources::Transactions.new
302
+ ```
303
+
304
+ ### Direct Resource Instantiation
305
+
306
+ For more advanced usage, you can instantiate resource classes directly:
307
+
308
+ ```ruby
309
+ # With a secret key
310
+ transactions = PaystackSdk::Resources::Transactions.new(secret_key: "sk_test_xxx")
311
+
312
+ # With an existing Faraday connection
313
+ connection = Faraday.new(url: "https://api.paystack.co") do |conn|
314
+ # Configure the connection
315
+ end
65
316
 
66
- puts response.original_response # => This will return the exact response received from Paystack
317
+ # The secret key can be omitted if set in an environment
318
+ transactions = PaystackSdk::Resources::Transactions.new(connection, secret_key:)
67
319
  ```
68
320
 
321
+ For more detailed documentation on specific resources, please refer to the following guides:
69
322
 
70
- Refer to the [documentation](https://github.com/nanafox/paystack_sdk) for more detailed usage examples and supported endpoints.
323
+ - [Transactions](https://paystack.com/docs/api/transaction/)
324
+ - [Customers](https://paystack.com/docs/api/customer/)
325
+ - [Plans](https://paystack.com/docs/api/plan/)
326
+ - [Subscriptions](https://paystack.com/docs/api/subscription/)
71
327
 
72
328
  ## Development
73
329
 
@@ -1,29 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "resources/transactions"
4
+ require_relative "utils/connection_utils"
4
5
 
5
6
  module PaystackSdk
6
7
  # The `Client` class serves as the main entry point for interacting with the Paystack API.
7
8
  # It initializes a connection to the Paystack API and provides access to various resources.
8
9
  class Client
9
- # The base URL for the Paystack API.
10
- BASE_URL = "https://api.paystack.co"
10
+ # Include connection utilities
11
+ include Utils::ConnectionUtils
12
+
13
+ # @return [Faraday::Connection] The Faraday connection object used for API requests
14
+ attr_reader :connection
11
15
 
12
16
  # Initializes a new `Client` instance.
13
17
  #
14
- # @param api_key [String] The secret API key for authenticating with the Paystack API.
18
+ # @param connection [Faraday::Connection, nil] The Faraday connection object used for API requests.
19
+ # If nil, a new connection will be created using the default API key.
20
+ # @param secret_key [String, nil] Optional API key to use for creating a new connection.
21
+ # Only used if connection is nil.
15
22
  #
16
- # @example
17
- # client = PaystackSdk::Client.new(api_key: "sk_test_xxx")
18
- def initialize(api_key:)
19
- @connection = Faraday.new(url: BASE_URL) do |conn|
20
- conn.request :json
21
- conn.response :json, content_type: /\bjson$/
22
- conn.headers["Authorization"] = "Bearer #{api_key}"
23
- conn.headers["Content-Type"] = "application/json"
24
- conn.headers["User-Agent"] = "paystack_sdk/#{PaystackSdk::VERSION}"
25
- conn.adapter Faraday.default_adapter
26
- end
23
+ # @example With an existing connection
24
+ # connection = Faraday.new(...)
25
+ # client = PaystackSdk::Client.new(connection)
26
+ #
27
+ # @example With an API key
28
+ # client = PaystackSdk::Client.new(secret_key: "sk_test_xxx")
29
+ #
30
+ # @example With default connection (requires PAYSTACK_SECRET_KEY environment variable)
31
+ # client = PaystackSdk::Client.new
32
+ def initialize(connection = nil, secret_key: nil)
33
+ @connection = initialize_connection(connection, secret_key: secret_key)
27
34
  end
28
35
 
29
36
  # Provides access to the `Transactions` resource.
@@ -32,8 +39,10 @@ module PaystackSdk
32
39
  # `Transactions` resource.
33
40
  #
34
41
  # @example
42
+ # ```ruby
35
43
  # transactions = client.transactions
36
- # response = transactions.initialize_transaction(params)
44
+ # response = transactions.initiate(params)
45
+ # ```
37
46
  def transactions
38
47
  @transactions ||= Resources::Transactions.new(@connection)
39
48
  end
@@ -1,25 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../response"
4
+ require_relative "../client"
5
+ require_relative "../validations"
6
+ require_relative "../utils/connection_utils"
4
7
 
5
8
  module PaystackSdk
6
9
  module Resources
7
10
  # The `Base` class serves as a parent class for all resource classes in the SDK.
8
11
  # It provides shared functionality, such as handling API responses.
9
12
  class Base
13
+ include PaystackSdk::Validations
14
+ include PaystackSdk::Utils::ConnectionUtils
15
+
10
16
  # Initializes a new `Base` instance.
11
17
  #
12
- # @param connection [Faraday::Connection] The Faraday connection object used for API requests.
13
- def initialize(connection)
14
- @connection = connection
18
+ # @param connection [Faraday::Connection, nil] The Faraday connection object used for API requests.
19
+ # If nil, a new connection will be created using the default API key.
20
+ # @param secret_key [String, nil] Optional API key to use for creating a new connection.
21
+ # Only used if connection is nil.
22
+ #
23
+ # @example With an existing connection
24
+ # connection = Faraday.new(...)
25
+ # resource = PaystackSdk::Resources::SomeResource.new(connection)
26
+ #
27
+ # @example With an API key
28
+ # resource = PaystackSdk::Resources::SomeResource.new(secret_key: "sk_test_xxx")
29
+ #
30
+ # @example With default connection (requires PAYSTACK_SECRET_KEY environment variable)
31
+ # resource = PaystackSdk::Resources::SomeResource.new
32
+ def initialize(connection = nil, secret_key: nil)
33
+ @connection = initialize_connection(connection, secret_key: secret_key)
15
34
  end
16
35
 
17
36
  private
18
37
 
19
- # Handles the API response, raising an error if the response is unsuccessful.
38
+ # Handles the API response, wrapping it in a Response object.
20
39
  #
21
40
  # @param response [Faraday::Response] The response object returned by the Faraday connection.
22
- # @return [PaystackSdk::Response] The parsed response body wrapped in a `PaystackSdk::Response` object if the request was successful.
41
+ # @return [PaystackSdk::Response] The parsed response body wrapped in a `PaystackSdk::Response` object.
23
42
  # @raise [PaystackSdk::Error] If the response indicates an error.
24
43
  def handle_response(response)
25
44
  PaystackSdk::Response.new(response)
@@ -12,20 +12,25 @@ module PaystackSdk
12
12
  #
13
13
  # Example usage:
14
14
  # ```ruby
15
- # transactions = PaystackSdk::Resources::Transactions.new(connection)
15
+ # transactions = PaystackSdk::Resources::Transactions.new(secret_key:)
16
16
  #
17
17
  # # Initialize a transaction
18
18
  # payload = { email: "customer@email.com", amount: 10000, currency: "GHS" }
19
- # response = transactions.initialize_transaction(payload)
19
+ # response = transactions.initiate(payload)
20
20
  # if response.success?
21
21
  # puts "Transaction initialized successfully."
22
- # puts "Authorization URL: #{response.data.authorization_url}"
22
+ # puts "Authorization URL: #{response.authorization_url}"
23
23
  # else
24
24
  # puts "Error initializing transaction: #{response.error_message}"
25
25
  # end
26
26
  #
27
27
  # # Verify a transaction
28
28
  # response = transactions.verify(reference: "transaction_reference")
29
+ # if response.status == "success"
30
+ # puts "The payment with reference '#{response.reference}' is verified"
31
+ # else
32
+ # puts "Current status: #{response.status}"
33
+ # end
29
34
  #
30
35
  # # List transactions
31
36
  # response = transactions.list(per_page: 50, page: 1)
@@ -40,14 +45,26 @@ module PaystackSdk
40
45
  # Initializes a new transaction.
41
46
  #
42
47
  # @param payload [Hash] The payload containing transaction details (e.g., email, amount, currency).
43
- # @return [Hash] The response from the Paystack API.
48
+ # @return [PaystackSdk::Response] The response from the Paystack API.
44
49
  # @raise [PaystackSdk::Error] If the payload is invalid or the API request fails.
45
50
  #
46
51
  # @example
52
+ # ```ruby
47
53
  # payload = { email: "customer@email.com", amount: 10000, currency: "GHS" }
48
- # response = transactions.initialize_transaction(payload)
54
+ # response = transactions.initiate(payload)
55
+ # ```
49
56
  def initiate(payload)
50
- raise PaystackSdk::Error, "Payload must be a hash" unless payload.is_a?(Hash)
57
+ validate_fields!(
58
+ payload: payload,
59
+ validations: {
60
+ email: {type: :email, required: true},
61
+ amount: {type: :positive_integer, required: true},
62
+ currency: {type: :currency, required: false},
63
+ reference: {type: :reference, required: false},
64
+ callback_url: {required: false}
65
+ }
66
+ )
67
+
51
68
  response = @connection.post("/transaction/initialize", payload)
52
69
  handle_response(response)
53
70
  end
@@ -55,12 +72,14 @@ module PaystackSdk
55
72
  # Verifies a transaction using its reference.
56
73
  #
57
74
  # @param reference [String] The unique reference for the transaction.
58
- # @return [Hash] The response from the Paystack API.
75
+ # @return [PaystackSdk::Response] The response from the Paystack API.
59
76
  # @raise [PaystackSdk::Error] If the API request fails.
60
77
  #
61
78
  # @example
62
- # response = transactions.verify("transaction_reference")
79
+ # response = transactions.verify(reference: "transaction_reference")
63
80
  def verify(reference:)
81
+ validate_presence!(value: reference, name: "Reference")
82
+
64
83
  response = @connection.get("/transaction/verify/#{reference}")
65
84
  handle_response(response)
66
85
  end
@@ -69,39 +88,208 @@ module PaystackSdk
69
88
  #
70
89
  # @param per_page [Integer] Number of records per page (default: 50)
71
90
  # @param page [Integer] Page number to retrieve (default: 1)
91
+ # @param from [String] A timestamp from which to start listing transactions e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
92
+ # @param to [String] A timestamp at which to stop listing transactions e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
93
+ # @param status [String] Filter transactions by status ('failed', 'success', 'abandoned')
94
+ # @param customer [Integer] Specify an ID for the customer whose transactions you want to retrieve
95
+ # @param currency [String] Specify the transaction currency to filter
96
+ # @param amount [Integer] Filter by transaction amount
72
97
  # @return [PaystackSdk::Response] The response from the Paystack API containing a
73
98
  # list of transactions.
74
99
  # @raise [PaystackSdk::Error] If the API request fails.
75
100
  #
76
101
  # @example
77
102
  # response = transactions.list(per_page: 20, page: 2)
78
- def list(per_page: 50, page: 1)
79
- response = @connection.get("/transaction", {perPage: per_page, page: page})
103
+ # # With filters
104
+ # response = transactions.list(per_page: 10, from: "2023-01-01", to: "2023-12-31", status: "success")
105
+ def list(per_page: 50, page: 1, **params)
106
+ # Create a combined parameter hash for validation
107
+ all_params = {per_page: per_page, page: page}.merge(params)
108
+
109
+ # Validate parameters
110
+ validate_fields!(
111
+ payload: all_params,
112
+ validations: {
113
+ per_page: {type: :positive_integer, required: false},
114
+ page: {type: :positive_integer, required: false},
115
+ from: {type: :date, required: false},
116
+ to: {type: :date, required: false},
117
+ status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false},
118
+ customer: {type: :positive_integer, required: false},
119
+ amount: {type: :positive_integer, required: false},
120
+ currency: {type: :currency, required: false}
121
+ }
122
+ )
123
+
124
+ # Prepare request parameters
125
+ request_params = {perPage: per_page, page: page}.merge(params)
126
+ response = @connection.get("/transaction", request_params)
80
127
  handle_response(response)
81
128
  end
82
129
 
83
130
  # Fetches details of a single transaction by its ID.
84
131
  #
85
- # @param transaction_id [Integer] The ID of the transaction to fetch.
86
- # @return [Hash] The response from the Paystack API containing transaction details.
132
+ # @param transaction_id [String, Integer] The ID of the transaction to fetch.
133
+ # @return [PaystackSdk::Response] The response from the Paystack API containing transaction details.
87
134
  # @raise [PaystackSdk::Error] If the API request fails.
88
135
  #
89
136
  # @example
90
- # response = transactions.fetch("transaction_id")
137
+ # response = transactions.fetch("12345")
91
138
  def fetch(transaction_id)
139
+ validate_presence!(value: transaction_id, name: "Transaction ID")
140
+
92
141
  response = @connection.get("/transaction/#{transaction_id}")
93
142
  handle_response(response)
94
143
  end
95
144
 
96
145
  # Fetches the totals of all transactions.
97
146
  #
98
- # @return [Hash] The response from the Paystack API containing transaction totals.
147
+ # @param from [String] A timestamp from which to start listing transaction totals e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
148
+ # @param to [String] A timestamp at which to stop listing transaction totals e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
149
+ # @return [PaystackSdk::Response] The response from the Paystack API containing transaction totals.
150
+ # @raise [PaystackSdk::Error] If the API request fails.
151
+ #
152
+ # @example
153
+ # response = transactions.totals
154
+ # # With date filters
155
+ # response = transactions.totals(from: "2023-01-01", to: "2023-12-31")
156
+ def totals(**params)
157
+ validate_fields!(
158
+ payload: params,
159
+ validations: {
160
+ from: {type: :date, required: false},
161
+ to: {type: :date, required: false}
162
+ }
163
+ )
164
+
165
+ response = @connection.get("/transaction/totals", params)
166
+ handle_response(response)
167
+ end
168
+
169
+ # Exports transactions as a downloadable file.
170
+ #
171
+ # @param from [String] A timestamp from which to start the export e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
172
+ # @param to [String] A timestamp at which to stop the export e.g. 2016-09-24T00:00:05.000Z, 2016-09-21
173
+ # @param status [String] Export only transactions with a specific status ('failed', 'success', 'abandoned')
174
+ # @param currency [String] Specify the transaction currency to export
175
+ # @param amount [Integer] Filter by transaction amount
176
+ # @param settled [Boolean] Set to true to export only settled transactions
177
+ # @param payment_page [Integer] Specify a payment page ID to export only transactions conducted through the page
178
+ # @param customer [Integer] Specify an ID for the customer whose transactions you want to export
179
+ # @param settlement [Integer] Specify a settlement ID to export only transactions in the settlement
180
+ # @return [PaystackSdk::Response] The response from the Paystack API containing the export details.
181
+ # @raise [PaystackSdk::Error] If the API request fails.
182
+ #
183
+ # @example
184
+ # response = transactions.export
185
+ # # With filters
186
+ # response = transactions.export(from: "2023-01-01", to: "2023-12-31", status: "success", currency: "NGN")
187
+ def export(**params)
188
+ validate_fields!(
189
+ payload: params,
190
+ validations: {
191
+ from: {type: :date, required: false},
192
+ to: {type: :date, required: false},
193
+ status: {type: :inclusion, allowed_values: %w[failed success abandoned], required: false},
194
+ currency: {type: :currency, required: false},
195
+ amount: {type: :positive_integer, required: false},
196
+ payment_page: {type: :positive_integer, required: false},
197
+ customer: {type: :positive_integer, required: false},
198
+ settlement: {type: :positive_integer, required: false}
199
+ }
200
+ )
201
+
202
+ response = @connection.get("/transaction/export", params)
203
+ handle_response(response)
204
+ end
205
+
206
+ # Charges an authorization code for subsequent payments.
207
+ #
208
+ # @param payload [Hash] The payload containing charge details.
209
+ # @option payload [String] :authorization_code Authorization code for the transaction (required)
210
+ # @option payload [String] :email Customer's email address (required)
211
+ # @option payload [Integer] :amount Amount in kobo, pesewas, or cents to charge (required)
212
+ # @option payload [String] :reference Unique transaction reference. Only -, ., = and alphanumeric characters allowed
213
+ # @option payload [String] :currency Currency in which amount should be charged (default: NGN)
214
+ # @option payload [Hash] :metadata Additional transaction information
215
+ # @option payload [Array<Hash>] :split_code Split payment among multiple accounts
216
+ # @return [PaystackSdk::Response] The response from the Paystack API.
217
+ # @raise [PaystackSdk::Error] If the payload is invalid or the API request fails.
218
+ #
219
+ # @example
220
+ # payload = {
221
+ # authorization_code: "AUTH_72btv547",
222
+ # email: "customer@email.com",
223
+ # amount: 10000
224
+ # }
225
+ # response = transactions.charge_authorization(payload)
226
+ def charge_authorization(payload)
227
+ validate_fields!(
228
+ payload: payload,
229
+ validations: {
230
+ authorization_code: {required: true},
231
+ email: {type: :email, required: true},
232
+ amount: {type: :positive_integer, required: true},
233
+ reference: {type: :reference, required: false},
234
+ currency: {type: :currency, required: false}
235
+ }
236
+ )
237
+
238
+ response = @connection.post("/transaction/charge_authorization", payload)
239
+ handle_response(response)
240
+ end
241
+
242
+ # Performs a partial debit on a customer's account.
243
+ #
244
+ # @param payload [Hash] The payload containing partial debit details.
245
+ # @option payload [String] :authorization_code Authorization code for the transaction (required)
246
+ # @option payload [String] :currency Currency in which amount should be charged (required)
247
+ # @option payload [Integer] :amount Amount in kobo, pesewas, or cents to charge (required)
248
+ # @option payload [String] :email Customer's email address (required)
249
+ # @option payload [String] :reference Unique transaction reference. Only -, ., = and alphanumeric characters allowed
250
+ # @option payload [Hash] :metadata Additional transaction information
251
+ # @option payload [Array<Hash>] :split_code Split payment among multiple accounts
252
+ # @return [PaystackSdk::Response] The response from the Paystack API.
253
+ # @raise [PaystackSdk::Error] If the payload is invalid or the API request fails.
254
+ #
255
+ # @example
256
+ # payload = {
257
+ # authorization_code: "AUTH_72btv547",
258
+ # currency: "NGN",
259
+ # amount: 10000,
260
+ # email: "customer@email.com"
261
+ # }
262
+ # response = transactions.partial_debit(payload)
263
+ def partial_debit(payload)
264
+ validate_fields!(
265
+ payload: payload,
266
+ validations: {
267
+ authorization_code: {required: true},
268
+ currency: {type: :currency, required: true},
269
+ amount: {type: :positive_integer, required: true},
270
+ email: {type: :email, required: true},
271
+ reference: {type: :reference, required: false}
272
+ }
273
+ )
274
+
275
+ response = @connection.post("/transaction/partial_debit", payload)
276
+ handle_response(response)
277
+ end
278
+
279
+ # View the timeline of a transaction
280
+ #
281
+ # @param id_or_reference [String] The ID or reference of the transaction
282
+ # @return [PaystackSdk::Response] The response from the Paystack API containing timeline details.
99
283
  # @raise [PaystackSdk::Error] If the API request fails.
100
284
  #
101
285
  # @example
102
- # response = transactions.totals
103
- def totals
104
- response = @connection.get("/transaction/totals")
286
+ # response = transactions.timeline("12345")
287
+ # # OR
288
+ # response = transactions.timeline("ref_123456789")
289
+ def timeline(id_or_reference)
290
+ validate_presence!(value: id_or_reference, name: "Transaction ID or Reference")
291
+
292
+ response = @connection.get("/transaction/timeline/#{id_or_reference}")
105
293
  handle_response(response)
106
294
  end
107
295
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaystackSdk
4
+ module Utils
5
+ # The `ConnectionUtils` module provides shared functionality for creating
6
+ # and initializing API connections. This is used by both the Client class
7
+ # and resource classes.
8
+ module ConnectionUtils
9
+ # The base URL for the Paystack API.
10
+ BASE_URL = "https://api.paystack.co"
11
+
12
+ # Initializes a connection based on the provided parameters.
13
+ #
14
+ # @param connection [Faraday::Connection, nil] An existing connection object.
15
+ # @param secret_key [String, nil] Optional API key to use for creating a new connection.
16
+ # @return [Faraday::Connection] A connection object for API requests.
17
+ # @raise [PaystackSdk::Error] If no connection or API key can be found.
18
+ def initialize_connection(connection = nil, secret_key: nil)
19
+ if connection
20
+ connection
21
+ elsif secret_key
22
+ create_connection(secret_key:)
23
+ else
24
+ # Try to get API key from environment variable
25
+ env_secret_key = ENV["PAYSTACK_SECRET_KEY"]
26
+ raise PaystackSdk::Error, "No connection or API key provided" unless env_secret_key
27
+
28
+ create_connection(secret_key: env_secret_key)
29
+ end
30
+ end
31
+
32
+ # Creates a new Faraday connection with the Paystack API.
33
+ #
34
+ # @param secret_key [String] The secret API key for authenticating with the Paystack API.
35
+ # @return [Faraday::Connection] A configured Faraday connection.
36
+ def create_connection(secret_key:)
37
+ Faraday.new(url: BASE_URL) do |conn|
38
+ conn.request :json
39
+ conn.response :json, content_type: /\bjson$/
40
+ conn.headers["Authorization"] = "Bearer #{secret_key}"
41
+ conn.headers["Content-Type"] = "application/json"
42
+ conn.headers["User-Agent"] = "paystack_sdk/#{PaystackSdk::VERSION}"
43
+ conn.adapter Faraday.default_adapter
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaystackSdk
4
+ # The Validations module provides shared validation methods for Paystack SDK resources.
5
+ # It includes methods for validating common parameters like references, amounts, dates, etc.
6
+ #
7
+ # This module is intended to be included in resource classes to provide consistent
8
+ # parameter validation before making API calls.
9
+ module Validations
10
+ # Validates that input is a hash.
11
+ #
12
+ # @param input [Object] The input to validate
13
+ # @param name [String] Name of the parameter for error messages
14
+ # @raise [PaystackSdk::Error] If input is not a hash
15
+ def validate_hash!(input:, name: "Payload")
16
+ raise PaystackSdk::Error, "#{name} must be a hash" unless input.is_a?(Hash)
17
+ end
18
+
19
+ # Validates that required parameters are present in a payload.
20
+ #
21
+ # @param payload [Hash] The payload to validate
22
+ # @param required_params [Array<Symbol>] List of required parameter keys
23
+ # @param operation_name [String] Name of the operation for error messages
24
+ # @raise [PaystackSdk::Error] If any required parameters are missing
25
+ def validate_required_params!(payload:, required_params:, operation_name: "Operation")
26
+ missing_params = required_params.select do |param|
27
+ !payload.key?(param) && !payload.key?(param.to_s)
28
+ end
29
+
30
+ 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}"
33
+ end
34
+ end
35
+
36
+ # Validates that a value is present (not nil or empty).
37
+ #
38
+ # @param value [Object] The value to validate
39
+ # @param name [String] Name of the parameter for error messages
40
+ # @raise [PaystackSdk::Error] If value is nil or empty
41
+ def validate_presence!(value:, name: "Parameter")
42
+ if value.nil? || (value.respond_to?(:empty?) && value.empty?)
43
+ raise PaystackSdk::Error, "#{name} cannot be empty"
44
+ end
45
+ end
46
+
47
+ # Validates that a number is a positive integer.
48
+ #
49
+ # @param value [Object] The value to validate
50
+ # @param name [String] Name of the parameter for error messages
51
+ # @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)
53
+ def validate_positive_integer!(value:, name: "Parameter", allow_nil: true)
54
+ if value.nil?
55
+ raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
56
+ elsif !value.is_a?(Integer) || value < 1
57
+ raise PaystackSdk::Error, "#{name} must be a positive integer"
58
+ end
59
+ end
60
+
61
+ # Validates a transaction reference format.
62
+ #
63
+ # @param reference [String] The reference to validate
64
+ # @param name [String] Name of the parameter for error messages
65
+ # @raise [PaystackSdk::Error] If reference format is invalid
66
+ def validate_reference_format!(reference:, name: "Reference")
67
+ unless reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/)
68
+ raise PaystackSdk::Error, "#{name} can only contain alphanumeric characters and the following: -, ., ="
69
+ end
70
+ end
71
+
72
+ # Validates a date string format.
73
+ #
74
+ # @param date_str [String] The date string to validate
75
+ # @param name [String] Name of the parameter for error messages
76
+ # @param allow_nil [Boolean] Whether nil values are allowed
77
+ # @raise [PaystackSdk::Error] If date format is invalid
78
+ def validate_date_format!(date_str:, name: "Date", allow_nil: true)
79
+ if date_str.nil?
80
+ raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
81
+ return
82
+ end
83
+
84
+ begin
85
+ Date.parse(date_str.to_s)
86
+ rescue Date::Error
87
+ raise PaystackSdk::Error, "Invalid #{name.downcase} format. Use format: YYYY-MM-DD or ISO8601"
88
+ end
89
+ end
90
+
91
+ # Validates that a value is within an allowed set of values.
92
+ #
93
+ # @param value [Object] The value to validate
94
+ # @param allowed_values [Array] List of allowed values
95
+ # @param name [String] Name of the parameter for error messages
96
+ # @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)
100
+ if value.nil?
101
+ raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
102
+ return
103
+ end
104
+
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}'"
111
+ end
112
+ end
113
+
114
+ # Validates an email format.
115
+ #
116
+ # @param email [String] The email to validate
117
+ # @param name [String] Name of the parameter for error messages
118
+ # @param allow_nil [Boolean] Whether nil values are allowed
119
+ # @raise [PaystackSdk::Error] If email format is invalid
120
+ def validate_email!(email:, name: "Email", allow_nil: false)
121
+ if email.nil?
122
+ raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
123
+ return
124
+ end
125
+
126
+ unless email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
127
+ raise PaystackSdk::Error, "Invalid #{name.downcase} format"
128
+ end
129
+ end
130
+
131
+ # Validates a currency code format.
132
+ #
133
+ # @param currency [String] The currency code to validate
134
+ # @param name [String] Name of the parameter for error messages
135
+ # @param allow_nil [Boolean] Whether nil values are allowed
136
+ # @raise [PaystackSdk::Error] If currency format is invalid
137
+ def validate_currency!(currency:, name: "Currency", allow_nil: true)
138
+ if currency.nil?
139
+ raise PaystackSdk::Error, "#{name} cannot be nil" unless allow_nil
140
+ return
141
+ end
142
+
143
+ 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)"
145
+ end
146
+ end
147
+
148
+ # Validates multiple fields at once.
149
+ #
150
+ # @param payload [Hash] The payload containing fields to validate
151
+ # @param validations [Hash] Mapping of field names to validation options
152
+ # @raise [PaystackSdk::Error] If any validation fails
153
+ #
154
+ # @example
155
+ # validate_fields!(
156
+ # payload: params,
157
+ # validations: {
158
+ # email: { type: :email, required: true },
159
+ # amount: { type: :positive_integer, required: true },
160
+ # currency: { type: :currency, required: false },
161
+ # reference: { type: :reference, required: false }
162
+ # }
163
+ # )
164
+ def validate_fields!(payload:, validations:)
165
+ # First check required fields
166
+ required_fields = validations.select { |_, opts| opts[:required] }.keys
167
+ validate_required_params!(payload: payload, required_params: required_fields) unless required_fields.empty?
168
+
169
+ # Then validate each field
170
+ validations.each do |field, options|
171
+ value = payload[field] || payload[field.to_s]
172
+ next if value.nil? && !options[:required]
173
+
174
+ case options[:type]
175
+ when :email
176
+ validate_email!(email: value, name: field.to_s.capitalize, allow_nil: !options[:required])
177
+ when :positive_integer
178
+ validate_positive_integer!(value: value, name: field.to_s, allow_nil: !options[:required])
179
+ when :reference
180
+ validate_reference_format!(reference: value, name: field.to_s) if value
181
+ when :date
182
+ validate_date_format!(date_str: value, name: field.to_s, allow_nil: !options[:required])
183
+ when :currency
184
+ validate_currency!(currency: value, name: field.to_s, allow_nil: !options[:required])
185
+ when :inclusion
186
+ if value
187
+ validate_inclusion!(
188
+ value: value,
189
+ allowed_values: options[:allowed_values],
190
+ name: field.to_s,
191
+ allow_nil: !options[:required],
192
+ case_sensitive: options[:case_sensitive]
193
+ )
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaystackSdk
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paystack_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxwell Nana Forson (theLazyProgrammer)
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-13 00:00:00.000000000 Z
11
+ date: 2025-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -119,6 +119,8 @@ files:
119
119
  - lib/paystack_sdk/resources/base.rb
120
120
  - lib/paystack_sdk/resources/transactions.rb
121
121
  - lib/paystack_sdk/response.rb
122
+ - lib/paystack_sdk/utils/connection_utils.rb
123
+ - lib/paystack_sdk/validations.rb
122
124
  - lib/paystack_sdk/version.rb
123
125
  - mise.toml
124
126
  - sig/paystack_sdk.rbs