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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a18abe45774bda30eff6f91a3e93ba55066d9ef4f04ddfc690792edc8b005d94
4
- data.tar.gz: 652cc2b936ac3a7211290ad6a6f561d57132fd825588232fab724a0b2378cfa7
3
+ metadata.gz: 7771d8c6ca4931f237e71c171c7507cfcd9ca5b6159974ee4a4a37d3d667a0e7
4
+ data.tar.gz: e2c83863b81cd30f7fed5380f145ccfa283a25ea334a30e33cb86170636f0b50
5
5
  SHA512:
6
- metadata.gz: 5fcb8324376b91af1c71e894309789fb69a4eeefff5d4a1fa93c190a66c9c01489d5a8a9b4003f8416fbff3ffe5d3474ec6ec49ab9c1a58ed7fb16ebf095c04a
7
- data.tar.gz: b094c91f15744f8c02ca9e493d7aa6ab7b9c24f6f0e25cc8e1a94d32bc9d3101aa713a97ff074000e365e1f1857d26735bd99016e82a480b7da839e6be9b349e
6
+ metadata.gz: aeeef1dcbfd223247142cfa6ee3ffb85624f2cc392ba5a993f9224ab5a4166273f386b1bf4a6fc4d0571dd30b3a626f08a1d0234cbe3808124e350c88228bebb
7
+ data.tar.gz: f8af0ecc7ef80cd7cfd0ab4858365bdce1b244115402869e5b1b29e3a6018816f19224ed0df2483d2fd732d6babd12a75764f4c324cdc55ee32bc35212d83654
@@ -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(&block)
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
- [:size, :length, :count, :empty?].each do |method_name|
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
- [:first, :last].each do |method_name|
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
- unless input.is_a?(Hash)
41
- raise PaystackSdk::InvalidFormatError.new(name, "Hash")
42
- end
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
- unless missing_params.empty?
57
- param = missing_params.first
58
- raise PaystackSdk::MissingParamError.new(param)
59
- end
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
- unless reference.to_s.match?(/^[a-zA-Z0-9._=-]+$/)
95
- raise PaystackSdk::InvalidFormatError.new(name, "alphanumeric characters and the following: -, ., =")
96
- end
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
- unless allowed_values.include?(value)
143
- allowed_list = allowed_values.join(", ")
144
- raise PaystackSdk::InvalidValueError.new(name, "must be one of: #{allowed_list}")
145
- end
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
- unless email.to_s.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
161
- raise PaystackSdk::InvalidFormatError.new(name, "valid email address")
162
- end
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
- unless currency.to_s.match?(/\A[A-Z]{3}\z/)
178
- raise PaystackSdk::InvalidFormatError.new(name, "3-letter ISO code (e.g., NGN, USD, GHS)")
179
- end
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaystackSdk
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
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.1.0
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: rspec
27
+ name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '3.13'
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: '3.13'
39
+ version: 1.9.0
40
40
  - !ruby/object:Gem::Dependency
41
- name: standard
41
+ name: irb
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.49.0
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.49.0
53
+ version: 1.15.1
54
54
  - !ruby/object:Gem::Dependency
55
- name: irb
55
+ name: rake
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 1.15.1
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: 1.15.1
67
+ version: 13.2.1
68
68
  - !ruby/object:Gem::Dependency
69
- name: rake
69
+ name: rspec
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 13.2.1
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.2.1
81
+ version: '3.13'
82
82
  - !ruby/object:Gem::Dependency
83
- name: debug
83
+ name: standard
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: 1.9.0
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.9.0
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