paystack_sdk 0.1.0 → 0.1.1
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 +4 -4
- data/lib/paystack_sdk/client.rb +15 -0
- data/lib/paystack_sdk/resources/charges.rb +105 -0
- data/lib/paystack_sdk/resources/customers.rb +2 -6
- data/lib/paystack_sdk/response.rb +7 -9
- data/lib/paystack_sdk/validations.rb +24 -20
- data/lib/paystack_sdk/version.rb +1 -1
- metadata +17 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7771d8c6ca4931f237e71c171c7507cfcd9ca5b6159974ee4a4a37d3d667a0e7
|
4
|
+
data.tar.gz: e2c83863b81cd30f7fed5380f145ccfa283a25ea334a30e33cb86170636f0b50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aeeef1dcbfd223247142cfa6ee3ffb85624f2cc392ba5a993f9224ab5a4166273f386b1bf4a6fc4d0571dd30b3a626f08a1d0234cbe3808124e350c88228bebb
|
7
|
+
data.tar.gz: f8af0ecc7ef80cd7cfd0ab4858365bdce1b244115402869e5b1b29e3a6018816f19224ed0df2483d2fd732d6babd12a75764f4c324cdc55ee32bc35212d83654
|
data/lib/paystack_sdk/client.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative "resources/transfer_recipients"
|
|
6
6
|
require_relative "resources/transfers"
|
7
7
|
require_relative "resources/banks"
|
8
8
|
require_relative "resources/verification"
|
9
|
+
require_relative "resources/charges"
|
9
10
|
require_relative "utils/connection_utils"
|
10
11
|
|
11
12
|
module PaystackSdk
|
@@ -121,5 +122,19 @@ module PaystackSdk
|
|
121
122
|
def verification
|
122
123
|
@verification ||= Resources::Verification.new(@connection)
|
123
124
|
end
|
125
|
+
|
126
|
+
# Provides access to the `Charges` resource.
|
127
|
+
#
|
128
|
+
# @return [PaystackSdk::Resources::Charges] An instance of the
|
129
|
+
# `Charges` resource.
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# ```ruby
|
133
|
+
# charges = client.charges
|
134
|
+
# response = charges.mobile_money(payload)
|
135
|
+
# ```
|
136
|
+
def charges
|
137
|
+
@charges ||= Resources::Charges.new(@connection)
|
138
|
+
end
|
124
139
|
end
|
125
140
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module PaystackSdk
|
6
|
+
module Resources
|
7
|
+
# The `Charges` resource exposes helpers for initiating and managing charges
|
8
|
+
# through alternative payment channels such as Mobile Money.
|
9
|
+
#
|
10
|
+
# At the moment the SDK focuses on supporting the Mobile Money channel which
|
11
|
+
# requires posting to the `/charge` endpoint with the customer's email,
|
12
|
+
# amount, currency, and the provider specific `mobile_money` payload.
|
13
|
+
class Charges < PaystackSdk::Resources::Base
|
14
|
+
MOBILE_MONEY_PROVIDERS = %w[mtn atl vod mpesa orange wave].freeze
|
15
|
+
|
16
|
+
# Initiates a Mobile Money payment.
|
17
|
+
#
|
18
|
+
# @param payload [Hash] The payload containing charge details.
|
19
|
+
# @option payload [String] :email Customer's email address (required)
|
20
|
+
# @option payload [Integer] :amount Amount in the lowest currency unit (required)
|
21
|
+
# @option payload [String] :currency ISO currency code (default: GHS)
|
22
|
+
# @option payload [String] :reference Optional reference supplied by the merchant
|
23
|
+
# @option payload [String] :callback_url Optional callback URL for Paystack to redirect to
|
24
|
+
# @option payload [Hash] :metadata Optional metadata to attach to the transaction
|
25
|
+
# @option payload [Hash] :mobile_money The mobile money details (required)
|
26
|
+
# - :phone [String] Customer's mobile money phone number (required)
|
27
|
+
# - :provider [String] Mobile money provider code (required)
|
28
|
+
#
|
29
|
+
# @return [PaystackSdk::Response] The wrapped API response.
|
30
|
+
# @raise [PaystackSdk::ValidationError] If the payload is invalid.
|
31
|
+
def mobile_money(payload)
|
32
|
+
validate_mobile_money_payload!(payload)
|
33
|
+
|
34
|
+
normalized_payload = normalize_mobile_money_provider(payload)
|
35
|
+
response = @connection.post("/charge", normalized_payload)
|
36
|
+
handle_response(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Submits an OTP for authorising a pending Mobile Money charge (e.g. Vodafone).
|
40
|
+
#
|
41
|
+
# @param payload [Hash] Payload containing the OTP and charge reference.
|
42
|
+
# @option payload [String] :otp The OTP supplied by the customer (required)
|
43
|
+
# @option payload [String] :reference The charge reference returned from initiation (required)
|
44
|
+
#
|
45
|
+
# @return [PaystackSdk::Response] The wrapped API response.
|
46
|
+
# @raise [PaystackSdk::ValidationError] If the payload is invalid.
|
47
|
+
def submit_otp(payload)
|
48
|
+
validate_fields!(
|
49
|
+
payload: payload,
|
50
|
+
validations: {
|
51
|
+
otp: {type: :string, required: true},
|
52
|
+
reference: {type: :reference, required: true}
|
53
|
+
}
|
54
|
+
)
|
55
|
+
|
56
|
+
response = @connection.post("/charge/submit_otp", payload)
|
57
|
+
handle_response(response)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def validate_mobile_money_payload!(payload)
|
63
|
+
validate_fields!(
|
64
|
+
payload: payload,
|
65
|
+
validations: {
|
66
|
+
email: {type: :email, required: true},
|
67
|
+
amount: {type: :positive_integer, required: true},
|
68
|
+
currency: {type: :currency, required: false},
|
69
|
+
reference: {type: :reference, required: false},
|
70
|
+
callback_url: {required: false},
|
71
|
+
metadata: {required: false},
|
72
|
+
mobile_money: {required: true}
|
73
|
+
}
|
74
|
+
)
|
75
|
+
|
76
|
+
mobile_money = payload[:mobile_money] || payload["mobile_money"]
|
77
|
+
validate_hash!(input: mobile_money, name: "mobile_money")
|
78
|
+
|
79
|
+
phone = mobile_money[:phone] || mobile_money["phone"]
|
80
|
+
validate_presence!(value: phone, name: "mobile_money phone")
|
81
|
+
|
82
|
+
provider = mobile_money[:provider] || mobile_money["provider"]
|
83
|
+
validate_mobile_money_provider!(provider)
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_mobile_money_provider!(provider)
|
87
|
+
normalized = provider&.to_s&.downcase
|
88
|
+
validate_allowed_values!(
|
89
|
+
value: normalized,
|
90
|
+
allowed_values: MOBILE_MONEY_PROVIDERS,
|
91
|
+
name: "mobile_money provider",
|
92
|
+
allow_nil: false
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def normalize_mobile_money_provider(payload)
|
97
|
+
mm = payload[:mobile_money] || payload["mobile_money"] || {}
|
98
|
+
provider = mm[:provider] || mm["provider"]
|
99
|
+
normalized_provider = provider&.to_s&.downcase
|
100
|
+
# Avoid mutating the caller's payload
|
101
|
+
payload.merge(mobile_money: mm.merge(provider: normalized_provider))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -68,13 +68,9 @@ module PaystackSdk
|
|
68
68
|
validate_positive_integer!(value: per_page, name: "per_page", allow_nil: true)
|
69
69
|
validate_positive_integer!(value: page, name: "page", allow_nil: true)
|
70
70
|
|
71
|
-
if params[:from]
|
72
|
-
validate_date_format!(date_str: params[:from], name: "from")
|
73
|
-
end
|
71
|
+
validate_date_format!(date_str: params[:from], name: "from") if params[:from]
|
74
72
|
|
75
|
-
if params[:to]
|
76
|
-
validate_date_format!(date_str: params[:to], name: "to")
|
77
|
-
end
|
73
|
+
validate_date_format!(date_str: params[:to], name: "to") if params[:to]
|
78
74
|
|
79
75
|
query_params = {perPage: per_page, page: page}.merge(params)
|
80
76
|
response = @connection.get("customer", query_params)
|
@@ -85,9 +85,7 @@ module PaystackSdk
|
|
85
85
|
@error_message = @api_message || "Client error"
|
86
86
|
|
87
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
|
+
raise AuthenticationError.new(@api_message || "Authentication failed") if @status_code == 401
|
91
89
|
when 429
|
92
90
|
# Rate limiting - raise as users need to implement retry logic
|
93
91
|
retry_after = response.headers["Retry-After"]
|
@@ -211,7 +209,7 @@ module PaystackSdk
|
|
211
209
|
# @yield [key, value] For hashes, passes each key-value pair
|
212
210
|
# @yield [value] For arrays, passes each item
|
213
211
|
# @return [Response, Enumerator] Self for chaining or Enumerator if no block given
|
214
|
-
def each
|
212
|
+
def each
|
215
213
|
return enum_for(:each) unless block_given?
|
216
214
|
|
217
215
|
if @raw_data.is_a?(Hash)
|
@@ -231,7 +229,7 @@ module PaystackSdk
|
|
231
229
|
# @return [Integer] The number of items
|
232
230
|
# @!method empty?
|
233
231
|
# @return [Boolean] Whether the collection is empty
|
234
|
-
[
|
232
|
+
%i[size length count empty?].each do |method_name|
|
235
233
|
define_method(method_name) do
|
236
234
|
@raw_data.send(method_name) if @raw_data.respond_to?(method_name)
|
237
235
|
end
|
@@ -242,9 +240,10 @@ module PaystackSdk
|
|
242
240
|
# @return [Object, Response] The first item, wrapped if necessary
|
243
241
|
# @!method last
|
244
242
|
# @return [Object, Response] The last item, wrapped if necessary
|
245
|
-
[
|
243
|
+
%i[first last].each do |method_name|
|
246
244
|
define_method(method_name) do
|
247
245
|
return nil unless @raw_data.is_a?(Array)
|
246
|
+
|
248
247
|
wrap_value(@raw_data.send(method_name))
|
249
248
|
end
|
250
249
|
end
|
@@ -261,9 +260,7 @@ module PaystackSdk
|
|
261
260
|
|
262
261
|
# First try to get identifier from the message
|
263
262
|
message = body["message"].to_s.downcase
|
264
|
-
if message =~ /with (id|code|reference|email): ([^\s]+)/i
|
265
|
-
return $2
|
266
|
-
end
|
263
|
+
return ::Regexp.last_match(2) if message =~ /with (id|code|reference|email): ([^\s]+)/i
|
267
264
|
|
268
265
|
# If not found in message, try to extract from error code
|
269
266
|
if body["code"]&.match?(/^(transaction|customer)_/)
|
@@ -288,6 +285,7 @@ module PaystackSdk
|
|
288
285
|
# @return [Hash, Array, nil] The data from the response
|
289
286
|
def extract_data_from_body(body)
|
290
287
|
return body unless body.is_a?(Hash)
|
288
|
+
|
291
289
|
body["data"] || body
|
292
290
|
end
|
293
291
|
|
@@ -37,9 +37,9 @@ module PaystackSdk
|
|
37
37
|
# @param name [String] Name of the parameter for error messages
|
38
38
|
# @raise [PaystackSdk::InvalidFormatError] If input is not a hash
|
39
39
|
def validate_hash!(input:, name: "Payload")
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
return if input.is_a?(Hash)
|
41
|
+
|
42
|
+
raise PaystackSdk::InvalidFormatError.new(name, "Hash")
|
43
43
|
end
|
44
44
|
|
45
45
|
# Validates that required parameters are present in a payload.
|
@@ -53,10 +53,10 @@ module PaystackSdk
|
|
53
53
|
!payload.key?(param) && !payload.key?(param.to_s)
|
54
54
|
end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
return if missing_params.empty?
|
57
|
+
|
58
|
+
param = missing_params.first
|
59
|
+
raise PaystackSdk::MissingParamError.new(param)
|
60
60
|
end
|
61
61
|
|
62
62
|
# Validates that a value is present (not nil or empty).
|
@@ -91,9 +91,9 @@ module PaystackSdk
|
|
91
91
|
# @param name [String] Name of the parameter for error messages
|
92
92
|
# @raise [PaystackSdk::InvalidFormatError] If reference format is invalid
|
93
93
|
def validate_reference_format!(reference:, name: "Reference")
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
return if reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/)
|
95
|
+
|
96
|
+
raise PaystackSdk::InvalidFormatError.new(name, "alphanumeric characters and the following: -, ., =")
|
97
97
|
end
|
98
98
|
|
99
99
|
# Validates a date string format.
|
@@ -106,6 +106,7 @@ module PaystackSdk
|
|
106
106
|
def validate_date_format!(date_str:, name: "Date", allow_nil: true)
|
107
107
|
if date_str.nil?
|
108
108
|
raise PaystackSdk::MissingParamError.new(name) unless allow_nil
|
109
|
+
|
109
110
|
return
|
110
111
|
end
|
111
112
|
|
@@ -136,13 +137,14 @@ module PaystackSdk
|
|
136
137
|
def validate_allowed_values!(value:, allowed_values:, name: "Parameter", allow_nil: true)
|
137
138
|
if value.nil?
|
138
139
|
raise PaystackSdk::MissingParamError.new(name) unless allow_nil
|
140
|
+
|
139
141
|
return
|
140
142
|
end
|
141
143
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
144
|
+
return if allowed_values.include?(value)
|
145
|
+
|
146
|
+
allowed_list = allowed_values.join(", ")
|
147
|
+
raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}")
|
146
148
|
end
|
147
149
|
|
148
150
|
# Validates an email format.
|
@@ -154,12 +156,13 @@ module PaystackSdk
|
|
154
156
|
def validate_email!(email:, name: "Email", allow_nil: false)
|
155
157
|
if email.nil?
|
156
158
|
raise PaystackSdk::MissingParamError.new(name) unless allow_nil
|
159
|
+
|
157
160
|
return
|
158
161
|
end
|
159
162
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
+
return if email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
164
|
+
|
165
|
+
raise PaystackSdk::InvalidFormatError.new(name, "valid email address")
|
163
166
|
end
|
164
167
|
|
165
168
|
# Validates a currency code format.
|
@@ -171,12 +174,13 @@ module PaystackSdk
|
|
171
174
|
def validate_currency!(currency:, name: "Currency", allow_nil: true)
|
172
175
|
if currency.nil?
|
173
176
|
raise PaystackSdk::MissingParamError.new(name) unless allow_nil
|
177
|
+
|
174
178
|
return
|
175
179
|
end
|
176
180
|
|
177
|
-
|
178
|
-
|
179
|
-
|
181
|
+
return if currency.to_s.match?(/\A[A-Z]{3}\z/)
|
182
|
+
|
183
|
+
raise PaystackSdk::InvalidFormatError.new(name, "3-letter ISO code (e.g., NGN, USD, GHS)")
|
180
184
|
end
|
181
185
|
|
182
186
|
# Validates multiple fields at once.
|
data/lib/paystack_sdk/version.rb
CHANGED
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.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maxwell Nana Forson (theLazyProgrammer)
|
@@ -24,75 +24,75 @@ dependencies:
|
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 2.13.1
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: debug
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 1.9.0
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 1.9.0
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
|
-
name:
|
41
|
+
name: irb
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: 1.
|
46
|
+
version: 1.15.1
|
47
47
|
type: :development
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 1.
|
53
|
+
version: 1.15.1
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
|
-
name:
|
55
|
+
name: rake
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
60
|
+
version: 13.2.1
|
61
61
|
type: :development
|
62
62
|
prerelease: false
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
67
|
+
version: 13.2.1
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
|
-
name:
|
69
|
+
name: rspec
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 13
|
74
|
+
version: '3.13'
|
75
75
|
type: :development
|
76
76
|
prerelease: false
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - "~>"
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: 13
|
81
|
+
version: '3.13'
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
|
-
name:
|
83
|
+
name: standard
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
|
-
version: 1.
|
88
|
+
version: 1.49.0
|
89
89
|
type: :development
|
90
90
|
prerelease: false
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version: 1.
|
95
|
+
version: 1.49.0
|
96
96
|
description: |
|
97
97
|
The `paystack_sdk` gem provides a simple and intuitive interface for
|
98
98
|
interacting with Paystack's payment gateway API. It allows developers to
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- lib/paystack_sdk/client.rb
|
118
118
|
- lib/paystack_sdk/resources/banks.rb
|
119
119
|
- lib/paystack_sdk/resources/base.rb
|
120
|
+
- lib/paystack_sdk/resources/charges.rb
|
120
121
|
- lib/paystack_sdk/resources/customers.rb
|
121
122
|
- lib/paystack_sdk/resources/transactions.rb
|
122
123
|
- lib/paystack_sdk/resources/transfer_recipients.rb
|