paystack_sdk 0.1.1 → 0.2.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.
@@ -1,46 +1,154 @@
1
- require_relative "../validations"
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
+ # Do not edit by hand: regenerate it with `bin/paystack-scaffold Transfer Recipient`. Hand-written extras go in lib/paystack_sdk/resources/extensions/transfer_recipients.rb.
5
+ # scaffold-digest: 74785aa38f6edfebdeff52d47bb3b355c2ad95b2cacdd6c0d7062725415bbfaa
6
+
7
+ require_relative "base"
2
8
 
3
9
  module PaystackSdk
4
10
  module Resources
5
- class TransferRecipients < Base
6
- # Create a transfer recipient
11
+ # Transfer Recipient operations.
12
+ class TransferRecipients < PaystackSdk::Resources::Base
13
+ # Ruby keyword => the parameter name Paystack documents.
14
+ WIRE_NAMES = {next_cursor: "next", per_page: "perPage"}.freeze
15
+
16
+ # List Transfer Recipients.
17
+ #
18
+ # List transfer recipients available on your integration
19
+ #
20
+ # @param use_cursor [Boolean] A flag to indicate if cursor based pagination should be used
21
+ # @param next_cursor [String] An alphanumeric value returned for every cursor based retrieval, used to retrieve the next set of data
22
+ # @param previous [String] An alphanumeric value returned for every cursor based retrieval, used to retrieve the previous set of data
23
+ # @param per_page [Integer] The number of records to fetch per request
24
+ # @param page [Integer] The offset to retrieve data from
25
+ # @return [PaystackSdk::Response] The response from the Paystack API.
26
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
27
+ # @see https://paystack.com/docs/api/transfer-recipient/#list
28
+ def list(use_cursor: nil, next_cursor: nil, previous: nil, per_page: nil, page: nil)
29
+ validate_positive_integer!(value: per_page, name: "perPage")
30
+ validate_positive_integer!(value: page, name: "page")
31
+
32
+ wire_query = to_wire({use_cursor:, next_cursor:, previous:, per_page:, page:}, WIRE_NAMES)
33
+
34
+ handle_response(@connection.get("/transferrecipient", wire_query))
35
+ end
36
+
37
+ # Create Transfer Recipient.
38
+ #
39
+ # Creates a new recipient. A duplicate account number will lead to the retrieval of the existing record.
40
+ #
41
+ # @param type [String] Recipient Type One of: nuban, ghipss, mobile_money, basa, authorization.
42
+ # @param name [String] The recipient's name according to their account registration.
43
+ # @param account_number [String] Recipient's bank account number
44
+ # @param bank_code [String] Recipient's bank code, from the List Banks endpoint.
45
+ # @param description [String] A description for this recipient
46
+ # @param currency [String] Currency for the account receiving the transfer
47
+ # @param authorization_code [String] An authorization code from a previous transaction
48
+ # @param metadata [Hash] JSON object of custom data
49
+ # @return [PaystackSdk::Response] The response from the Paystack API.
50
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
7
51
  # @see https://paystack.com/docs/api/transfer-recipient/#create
8
- def create(params)
9
- validate_hash!(input: params, name: "TransferRecipient params")
10
- validate_required_params!(
11
- payload: params,
12
- required_params: %i[type name account_number bank_code],
13
- operation_name: "Create Transfer Recipient"
52
+ def create(
53
+ type:,
54
+ name:,
55
+ account_number:,
56
+ bank_code:,
57
+ description: nil,
58
+ currency: nil,
59
+ authorization_code: nil,
60
+ metadata: nil
61
+ )
62
+ validate_presence!(value: type, name: "type")
63
+ validate_allowed_values!(
64
+ value: type,
65
+ allowed_values: %w[nuban ghipss mobile_money basa authorization],
66
+ name: "type"
67
+ )
68
+ validate_presence!(value: name, name: "name")
69
+ validate_presence!(value: account_number, name: "account_number")
70
+ validate_presence!(value: bank_code, name: "bank_code")
71
+
72
+ wire_body = to_wire(
73
+ {
74
+ type:,
75
+ name:,
76
+ account_number:,
77
+ bank_code:,
78
+ description:,
79
+ currency:,
80
+ authorization_code:,
81
+ metadata:
82
+ },
83
+ WIRE_NAMES
14
84
  )
15
- handle_response(@connection.post("/transferrecipient", params))
85
+
86
+ handle_response(@connection.post("/transferrecipient", wire_body))
16
87
  end
17
88
 
18
- # List transfer recipients
19
- # @see https://paystack.com/docs/api/transfer-recipient/#list
20
- def list(query = {})
21
- handle_response(@connection.get("/transferrecipient", query))
89
+ # Bulk Create Transfer Recipient.
90
+ #
91
+ # Create multiple transfer recipients in batches. A duplicate account number will lead to the retrieval of the existing record.
92
+ #
93
+ # @param batch [Array<Hash>] A list of transfer recipient objects, each with the fields accepted by {#create}
94
+ # @return [PaystackSdk::Response] The response from the Paystack API.
95
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
96
+ # @see https://paystack.com/docs/api/transfer-recipient/#bulk
97
+ def bulk_create(batch:)
98
+ validate_presence!(value: batch, name: "batch")
99
+
100
+ wire_body = to_wire({batch:}, WIRE_NAMES)
101
+
102
+ handle_response(@connection.post("/transferrecipient/bulk", wire_body))
22
103
  end
23
104
 
24
- # Fetch a transfer recipient
105
+ # Fetch Transfer recipient.
106
+ #
107
+ # Fetch the details of a transfer recipient
108
+ #
109
+ # @param id_or_code [String] The recipient code (RCP_...) or numeric ID
110
+ # @return [PaystackSdk::Response] The response from the Paystack API.
111
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
25
112
  # @see https://paystack.com/docs/api/transfer-recipient/#fetch
26
- def fetch(recipient_code:)
27
- validate_presence!(value: recipient_code, name: "recipient_code")
28
- handle_response(@connection.get("/transferrecipient/#{recipient_code}"))
113
+ def fetch(id_or_code:)
114
+ validate_presence!(value: id_or_code, name: "id_or_code")
115
+
116
+ handle_response(@connection.get("/transferrecipient/#{escape_path(id_or_code, name: "id_or_code")}"))
29
117
  end
30
118
 
31
- # Update a transfer recipient
119
+ # Update Transfer Recipient.
120
+ #
121
+ # Update the details of a transfer recipient
122
+ #
123
+ # @param id_or_code [String] The recipient code (RCP_...) or numeric ID
124
+ # @param name [String] Recipient's name
125
+ # @param email [String] Recipient's email address
126
+ # @return [PaystackSdk::Response] The response from the Paystack API.
127
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
32
128
  # @see https://paystack.com/docs/api/transfer-recipient/#update
33
- def update(recipient_code:, params:)
34
- validate_presence!(value: recipient_code, name: "recipient_code")
35
- validate_hash!(input: params, name: "Update TransferRecipient params")
36
- handle_response(@connection.put("/transferrecipient/#{recipient_code}", params))
129
+ def update(id_or_code:, name: nil, email: nil)
130
+ validate_presence!(value: id_or_code, name: "id_or_code")
131
+ validate_email!(email: email, name: "email", allow_nil: true)
132
+
133
+ wire_body = to_wire({name:, email:}, WIRE_NAMES)
134
+
135
+ handle_response(
136
+ @connection.put("/transferrecipient/#{escape_path(id_or_code, name: "id_or_code")}", wire_body)
137
+ )
37
138
  end
38
139
 
39
- # Delete a transfer recipient
140
+ # Delete Transfer Recipient.
141
+ #
142
+ # Delete a transfer recipient (sets the transfer recipient to inactive)
143
+ #
144
+ # @param id_or_code [String] The recipient code (RCP_...) or numeric ID
145
+ # @return [PaystackSdk::Response] The response from the Paystack API.
146
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
40
147
  # @see https://paystack.com/docs/api/transfer-recipient/#delete
41
- def delete(recipient_code:)
42
- validate_presence!(value: recipient_code, name: "recipient_code")
43
- handle_response(@connection.delete("/transferrecipient/#{recipient_code}"))
148
+ def delete(id_or_code:)
149
+ validate_presence!(value: id_or_code, name: "id_or_code")
150
+
151
+ handle_response(@connection.delete("/transferrecipient/#{escape_path(id_or_code, name: "id_or_code")}"))
44
152
  end
45
153
  end
46
154
  end
@@ -1,46 +1,281 @@
1
- require_relative "../validations"
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d), then
4
+ # edited by hand against Paystack's Transfers and Transfers Control docs pages (descriptions, docs
5
+ # links and the `fetch(id_or_code:)` keyword), so the scaffold treats it as hand-written and will not regenerate it.
6
+ # Hand-written extras go in lib/paystack_sdk/resources/extensions/transfers.rb.
7
+
8
+ require_relative "base"
2
9
 
3
10
  module PaystackSdk
4
11
  module Resources
5
- class Transfers < Base
6
- # Create a transfer
7
- # @see https://paystack.com/docs/api/transfer/#initiate
8
- def create(params)
9
- validate_hash!(input: params, name: "Transfer params")
10
- validate_required_params!(
11
- payload: params,
12
- required_params: %i[source amount recipient],
13
- operation_name: "Create Transfer"
14
- )
15
- handle_response(@connection.post("/transfer", params))
16
- end
12
+ # Transfer operations: send money from your balance to transfer recipients, and manage the OTP
13
+ # requirement that guards them.
14
+ class Transfers < PaystackSdk::Resources::Base
15
+ # Ruby keyword => the parameter name Paystack documents.
16
+ WIRE_NAMES = {next_cursor: "next", per_page: "perPage"}.freeze
17
17
 
18
- # List transfers
18
+ # List Transfers.
19
+ #
20
+ # List the transfers made on your integration.
21
+ #
22
+ # @param use_cursor [Boolean] Set to true to use cursor-based pagination (the response meta then carries `next` and `previous`)
23
+ # @param next_cursor [String] The `next` cursor from a previous cursor-based response
24
+ # @param previous [String] The `previous` cursor from a previous cursor-based response
25
+ # @param per_page [Integer] How many records to retrieve per page (Paystack defaults to 50)
26
+ # @param page [Integer] The page to retrieve (Paystack defaults to 1)
27
+ # @param from [String, Date, Time] A timestamp from which to start listing transfers
28
+ # @param to [String, Date, Time] A timestamp at which to stop listing transfers
29
+ # @param recipient [Integer] Filter by the recipient ID
30
+ # @param status [String] Filter by status. One of: pending, success, failed, otp, abandoned, reversed, blocked, rejected, received.
31
+ # @return [PaystackSdk::Response] The response from the Paystack API.
32
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
19
33
  # @see https://paystack.com/docs/api/transfer/#list
20
- def list(query = {})
21
- handle_response(@connection.get("/transfer", query))
34
+ def list(
35
+ use_cursor: nil,
36
+ next_cursor: nil,
37
+ previous: nil,
38
+ per_page: nil,
39
+ page: nil,
40
+ from: nil,
41
+ to: nil,
42
+ recipient: nil,
43
+ status: nil
44
+ )
45
+ validate_positive_integer!(value: per_page, name: "perPage")
46
+ validate_positive_integer!(value: page, name: "page")
47
+ validate_allowed_values!(
48
+ value: status,
49
+ allowed_values: %w[pending success failed otp abandoned reversed blocked rejected received],
50
+ name: "status"
51
+ )
52
+
53
+ wire_query = to_wire(
54
+ {
55
+ use_cursor:,
56
+ next_cursor:,
57
+ previous:,
58
+ per_page:,
59
+ page:,
60
+ from: format_datetime(from, name: "from"),
61
+ to: format_datetime(to, name: "to"),
62
+ recipient:,
63
+ status:
64
+ },
65
+ WIRE_NAMES
66
+ )
67
+
68
+ handle_response(@connection.get("/transfer", wire_query))
22
69
  end
23
70
 
24
- # Fetch a transfer
25
- # @see https://paystack.com/docs/api/transfer/#fetch
26
- def fetch(id:)
27
- validate_presence!(value: id, name: "transfer id")
28
- handle_response(@connection.get("/transfer/#{id}"))
71
+ # Initiate Transfer.
72
+ #
73
+ # Send money to your customers. The transfer's status is `pending` when the OTP requirement is
74
+ # disabled, and `otp` when an OTP is required (complete it with {#finalize}).
75
+ #
76
+ # @param amount [Integer] Amount to transfer in the currency's subunit (kobo for NGN, pesewas for GHS)
77
+ # @param recipient [String] Code for the transfer recipient (RCP_...)
78
+ # @param reference [String] A unique identifier for the transfer, so retrying it cannot create a second transfer. Paystack documents 16 to 50 characters of lowercase letters, digits, `-` and `_`.
79
+ # @param source [String] Where to transfer from. Only "balance" for now.
80
+ # @param reason [String] The reason for the transfer; it also shows up in the narration of the recipient's credit notification
81
+ # @param currency [String] The currency of the transfer; Paystack defaults to NGN. One of: NGN, ZAR, KES, GHS.
82
+ # @return [PaystackSdk::Response] The response from the Paystack API.
83
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
84
+ # @see https://paystack.com/docs/api/transfer/#initiate
85
+ def create(amount:, recipient:, reference:, source:, reason: nil, currency: nil)
86
+ validate_presence!(value: amount, name: "amount")
87
+ validate_positive_integer!(value: amount, name: "amount")
88
+ validate_presence!(value: recipient, name: "recipient")
89
+ validate_presence!(value: reference, name: "reference")
90
+ validate_reference_format!(reference: reference, name: "reference")
91
+ validate_presence!(value: source, name: "source")
92
+ validate_allowed_values!(
93
+ value: currency,
94
+ allowed_values: %w[NGN ZAR KES GHS],
95
+ name: "currency"
96
+ )
97
+
98
+ wire_body = to_wire(
99
+ {
100
+ amount:,
101
+ recipient:,
102
+ reference:,
103
+ source:,
104
+ reason:,
105
+ currency:
106
+ },
107
+ WIRE_NAMES
108
+ )
109
+
110
+ handle_response(@connection.post("/transfer", wire_body))
29
111
  end
30
112
 
31
- # Finalize a transfer (OTP)
113
+ # Finalize Transfer.
114
+ #
115
+ # Finalize an initiated transfer that is waiting for an OTP.
116
+ #
117
+ # @param transfer_code [String] The transfer code you want to finalize
118
+ # @param otp [String] OTP sent to the business phone to verify the transfer
119
+ # @return [PaystackSdk::Response] The response from the Paystack API.
120
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
32
121
  # @see https://paystack.com/docs/api/transfer/#finalize
33
122
  def finalize(transfer_code:, otp:)
34
123
  validate_presence!(value: transfer_code, name: "transfer_code")
35
124
  validate_presence!(value: otp, name: "otp")
36
- handle_response(@connection.post("/transfer/finalize_transfer", {transfer_code: transfer_code, otp: otp}))
125
+
126
+ wire_body = to_wire({transfer_code:, otp:}, WIRE_NAMES)
127
+
128
+ handle_response(@connection.post("/transfer/finalize_transfer", wire_body))
129
+ end
130
+
131
+ # Initiate Bulk Transfer.
132
+ #
133
+ # Batch multiple transfers in a single request. You need to disable the Transfers OTP
134
+ # requirement to use this endpoint.
135
+ #
136
+ # @param source [String] Where to transfer from. Only "balance" for now.
137
+ # @param transfers [Array<Hash>] The transfers, each with `amount`, `recipient`, `reference` and optionally `reason`, named as Paystack names them
138
+ # @param currency [String] The currency of the transfers. One of: NGN, ZAR, KES, GHS.
139
+ # @return [PaystackSdk::Response] The response from the Paystack API.
140
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
141
+ # @see https://paystack.com/docs/api/transfer/#bulk
142
+ def bulk_create(source:, transfers:, currency: nil)
143
+ validate_presence!(value: source, name: "source")
144
+ validate_presence!(value: transfers, name: "transfers")
145
+ validate_allowed_values!(
146
+ value: currency,
147
+ allowed_values: %w[NGN ZAR KES GHS],
148
+ name: "currency"
149
+ )
150
+
151
+ wire_body = to_wire({source:, transfers:, currency:}, WIRE_NAMES)
152
+
153
+ handle_response(@connection.post("/transfer/bulk", wire_body))
154
+ end
155
+
156
+ # Fetch Transfer.
157
+ #
158
+ # Get details of a transfer on your integration.
159
+ #
160
+ # @param id_or_code [String, Integer] The transfer ID or code you want to fetch
161
+ # @return [PaystackSdk::Response] The response from the Paystack API.
162
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
163
+ # @see https://paystack.com/docs/api/transfer/#fetch
164
+ def fetch(id_or_code:)
165
+ validate_presence!(value: id_or_code, name: "id_or_code")
166
+
167
+ handle_response(@connection.get("/transfer/#{escape_path(id_or_code, name: "id_or_code")}"))
37
168
  end
38
169
 
39
- # Verify a transfer
170
+ # Verify Transfer.
171
+ #
172
+ # Verify the status of a transfer on your integration.
173
+ #
174
+ # @param reference [String] Transfer reference
175
+ # @return [PaystackSdk::Response] The response from the Paystack API.
176
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
40
177
  # @see https://paystack.com/docs/api/transfer/#verify
41
178
  def verify(reference:)
42
179
  validate_presence!(value: reference, name: "reference")
43
- handle_response(@connection.get("/transfer/verify/#{reference}"))
180
+
181
+ handle_response(
182
+ @connection.get("/transfer/verify/#{escape_path(reference, name: "reference")}")
183
+ )
184
+ end
185
+
186
+ # Export Transfers.
187
+ #
188
+ # Export a list of transfers carried out on your integration. This operation is in Paystack's
189
+ # OpenAPI spec but not on its docs page; the API answers it (404 "Transfers not found" when
190
+ # nothing matches).
191
+ #
192
+ # @param recipient [String] Export transfers by the recipient code
193
+ # @param status [String] Export transfers by status. One of: pending, success, failed, otp, abandoned, reversed, blocked, rejected, received.
194
+ # @param from [String, Date, Time] The start date
195
+ # @param to [String, Date, Time] The end date
196
+ # @return [PaystackSdk::Response] The response from the Paystack API.
197
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
198
+ # @see https://paystack.com/docs/api/transfer/
199
+ def export(recipient: nil, status: nil, from: nil, to: nil)
200
+ validate_allowed_values!(
201
+ value: status,
202
+ allowed_values: %w[pending success failed otp abandoned reversed blocked rejected received],
203
+ name: "status"
204
+ )
205
+
206
+ wire_query = to_wire(
207
+ {
208
+ recipient:,
209
+ status:,
210
+ from: format_datetime(from, name: "from"),
211
+ to: format_datetime(to, name: "to")
212
+ },
213
+ WIRE_NAMES
214
+ )
215
+
216
+ handle_response(@connection.get("/transfer/export", wire_query))
217
+ end
218
+
219
+ # Resend OTP for Transfer.
220
+ #
221
+ # Generates a new OTP and sends it to the business phone, for when it has trouble receiving one.
222
+ #
223
+ # @param transfer_code [String] The transfer code that requires an OTP validation
224
+ # @param reason [String] The purpose of the OTP. The docs list "resend_otp" and "transfer"; the spec also allows "disable_otp".
225
+ # @return [PaystackSdk::Response] The response from the Paystack API.
226
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
227
+ # @see https://paystack.com/docs/api/transfer-control/#resend-otp
228
+ def resend_otp(transfer_code:, reason:)
229
+ validate_presence!(value: transfer_code, name: "transfer_code")
230
+ validate_presence!(value: reason, name: "reason")
231
+ validate_allowed_values!(
232
+ value: reason,
233
+ allowed_values: %w[disable_otp resend_otp transfer],
234
+ name: "reason"
235
+ )
236
+
237
+ wire_body = to_wire({transfer_code:, reason:}, WIRE_NAMES)
238
+
239
+ handle_response(@connection.post("/transfer/resend_otp", wire_body))
240
+ end
241
+
242
+ # Disable OTP for Transfers.
243
+ #
244
+ # Start turning off the OTP requirement so transfers can be completed programmatically.
245
+ # Paystack sends an OTP to the business phone; confirm with {#finalize_disable_otp}.
246
+ #
247
+ # @return [PaystackSdk::Response] The response from the Paystack API.
248
+ # @raise [PaystackSdk::Error] If the API request fails.
249
+ # @see https://paystack.com/docs/api/transfer-control/#disable-otp
250
+ def disable_otp
251
+ handle_response(@connection.post("/transfer/disable_otp"))
252
+ end
253
+
254
+ # Finalize Disabling OTP for Transfers.
255
+ #
256
+ # Finalize the request to disable OTP on your transfers.
257
+ #
258
+ # @param otp [String] OTP sent to the business phone to verify disabling the OTP requirement
259
+ # @return [PaystackSdk::Response] The response from the Paystack API.
260
+ # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
261
+ # @see https://paystack.com/docs/api/transfer-control/#finalize-disable-otp
262
+ def finalize_disable_otp(otp:)
263
+ validate_presence!(value: otp, name: "otp")
264
+
265
+ wire_body = to_wire({otp:}, WIRE_NAMES)
266
+
267
+ handle_response(@connection.post("/transfer/disable_otp_finalize", wire_body))
268
+ end
269
+
270
+ # Enable OTP requirement for Transfers.
271
+ #
272
+ # Turn the OTP requirement for transfers back on.
273
+ #
274
+ # @return [PaystackSdk::Response] The response from the Paystack API.
275
+ # @raise [PaystackSdk::Error] If the API request fails.
276
+ # @see https://paystack.com/docs/api/transfer-control/#enable-otp
277
+ def enable_otp
278
+ handle_response(@connection.post("/transfer/enable_otp"))
44
279
  end
45
280
  end
46
281
  end
@@ -60,6 +60,19 @@ module PaystackSdk
60
60
  # @return [Integer] The status code of the API response
61
61
  attr_reader :status_code
62
62
 
63
+ # Pagination metadata that Paystack returns with list responses
64
+ # (e.g. `total`, `page`, `pageCount`, `perPage`).
65
+ #
66
+ # @return [Response, nil] The wrapped `meta` object, or nil if the response has none
67
+ #
68
+ # @example
69
+ # response = transactions.list
70
+ # response.meta.total # => 40
71
+ # response.meta.pageCount # => 2
72
+ def meta
73
+ @meta ||= wrap_value(@raw_meta) if @raw_meta
74
+ end
75
+
63
76
  # Initializes a new Response object
64
77
  #
65
78
  # @param response [Faraday::Response, Hash, Array] The raw API response or data
@@ -75,10 +88,15 @@ module PaystackSdk
75
88
  @api_message = extract_api_message(@body)
76
89
  @message = @api_message
77
90
  @raw_data = extract_data_from_body(@body)
91
+ @raw_meta = @body["meta"] if @body.is_a?(Hash) && @body["meta"].is_a?(Hash)
78
92
 
79
93
  case @status_code
80
94
  when 200..299
81
95
  @success = true
96
+ when 429
97
+ # Rate limiting - raise so callers can back off (the connection
98
+ # already retries automatically unless max_retries is 0)
99
+ raise RateLimitError.new(rate_limit_reset(response))
82
100
  when 400..499
83
101
  # Client errors - return unsuccessful response for user to handle
84
102
  @success = false
@@ -86,10 +104,6 @@ module PaystackSdk
86
104
 
87
105
  # Still raise for authentication issues as these are usually config problems
88
106
  raise AuthenticationError.new(@api_message || "Authentication failed") if @status_code == 401
89
- when 429
90
- # Rate limiting - raise as users need to implement retry logic
91
- retry_after = response.headers["Retry-After"]
92
- raise RateLimitError.new(retry_after || 30)
93
107
  when 500..599
94
108
  # Server errors - raise as these indicate Paystack infrastructure issues
95
109
  raise ServerError.new(@status_code, @api_message)
@@ -102,6 +116,7 @@ module PaystackSdk
102
116
  @error_message = response.error_message
103
117
  @api_message = response.api_message
104
118
  @raw_data = response.raw_data
119
+ @raw_meta = response.raw_meta
105
120
  else
106
121
  @success = true
107
122
  @raw_data = response
@@ -123,6 +138,34 @@ module PaystackSdk
123
138
  @success
124
139
  end
125
140
 
141
+ # Whether this response is a successful payment: the call succeeded and the transaction's
142
+ # `status` is "success". Paystack's docs say to confirm the amount and currency as well, so
143
+ # pass the ones you expect and they are compared too.
144
+ #
145
+ # @param amount [Integer, nil] The amount (in the currency's subunit) you expect
146
+ # @param currency [String, nil] The currency you expect, e.g. "GHS"
147
+ # @return [Boolean]
148
+ #
149
+ # @example
150
+ # response = client.transactions.verify(reference: ref)
151
+ # response.paid?(amount: 5000, currency: "GHS")
152
+ def paid?(amount: nil, currency: nil)
153
+ return false unless success? && status?(:success)
154
+ return false if amount && field(:amount) != amount
155
+ return false if currency && field(:currency).to_s.upcase != currency.to_s.upcase
156
+
157
+ true
158
+ end
159
+
160
+ # Whether the `status` field in the response data equals the given value, e.g.
161
+ # `response.status?(:send_pin)` on a charge. Paystack names the values; none are assumed here.
162
+ #
163
+ # @param value [String, Symbol]
164
+ # @return [Boolean]
165
+ def status?(value)
166
+ field(:status).to_s == value.to_s
167
+ end
168
+
126
169
  # Check if the response failed
127
170
  #
128
171
  # @return [Boolean] true if the API request failed
@@ -151,6 +194,14 @@ module PaystackSdk
151
194
  @body
152
195
  end
153
196
 
197
+ # One field of a Hash response body, whichever way it is keyed; nil otherwise.
198
+ def field(name)
199
+ return nil unless @raw_data.is_a?(Hash)
200
+
201
+ @raw_data.key?(name) ? @raw_data[name] : @raw_data[name.to_s]
202
+ end
203
+ private :field
204
+
154
205
  # Access hash values via methods (dot notation)
155
206
  # Allows accessing data attributes directly: response.attribute_name
156
207
  #
@@ -181,7 +232,8 @@ module PaystackSdk
181
232
  super
182
233
  end
183
234
 
184
- # Access data via hash/array notation
235
+ # Access data via hash/array notation.
236
+ # Hash keys can be given as strings or symbols, whichever way the data is keyed.
185
237
  #
186
238
  # @param key [Object] The key or index to access
187
239
  # @return [Object, Response] The value for the given key or index
@@ -189,19 +241,19 @@ module PaystackSdk
189
241
  return nil unless @raw_data
190
242
 
191
243
  if @raw_data.is_a?(Hash)
192
- value = @raw_data[key.is_a?(String) ? key.to_sym : key]
193
- wrap_value(value)
244
+ actual_key = lookup_key(key)
245
+ wrap_value(@raw_data[actual_key]) unless actual_key.nil?
194
246
  elsif @raw_data.is_a?(Array) && key.is_a?(Integer)
195
247
  wrap_value(@raw_data[key])
196
248
  end
197
249
  end
198
250
 
199
- # Check if key exists in hash
251
+ # Check if key exists in hash (as a string or a symbol)
200
252
  #
201
253
  # @param key [Symbol, String] The key to check
202
254
  # @return [Boolean] Whether the key exists
203
255
  def key?(key)
204
- @raw_data.is_a?(Hash) && @raw_data.key?(key.is_a?(String) ? key.to_sym : key)
256
+ @raw_data.is_a?(Hash) && !lookup_key(key).nil?
205
257
  end
206
258
 
207
259
  # Iterate through hash entries or array items
@@ -248,8 +300,35 @@ module PaystackSdk
248
300
  end
249
301
  end
250
302
 
303
+ protected
304
+
305
+ # @return [Hash, nil] The unwrapped `meta` from the response body
306
+ attr_reader :raw_meta
307
+
251
308
  private
252
309
 
310
+ # Finds the key actually used in the data for a string or symbol lookup.
311
+ #
312
+ # @return [Object, nil] The key present in the data, or nil if there is none
313
+ def lookup_key(key)
314
+ return key if @raw_data.key?(key)
315
+
316
+ alternate = case key
317
+ when String then key.to_sym
318
+ when Symbol then key.to_s
319
+ end
320
+ alternate if !alternate.nil? && @raw_data.key?(alternate)
321
+ end
322
+
323
+ # Seconds until the rate-limit window ends, from Paystack's
324
+ # `x-ratelimit-reset` header (nil if absent, not numeric, negative or not finite).
325
+ def rate_limit_reset(response)
326
+ seconds = Float(response.headers["x-ratelimit-reset"])
327
+ (seconds.finite? && seconds >= 0) ? seconds.ceil : nil
328
+ rescue ArgumentError, TypeError
329
+ nil
330
+ end
331
+
253
332
  # Extract the identifier from an error response
254
333
  # This looks for common patterns in error messages to find resource identifiers
255
334
  #