paytree 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +36 -37
- data/README.md +99 -3
- data/lib/paytree/configs/mpesa.rb +4 -11
- data/lib/paytree/mpesa/adapters/daraja/b2b.rb +4 -2
- data/lib/paytree/mpesa/adapters/daraja/b2c.rb +10 -3
- data/lib/paytree/mpesa/adapters/daraja/base.rb +19 -87
- data/lib/paytree/mpesa/adapters/daraja/http_client_factory.rb +62 -0
- data/lib/paytree/mpesa/adapters/daraja/response_helpers.rb +12 -2
- data/lib/paytree/mpesa/adapters/daraja/token_manager.rb +74 -0
- data/lib/paytree/mpesa/adapters/daraja/validator.rb +89 -0
- data/lib/paytree/utils/error_handling.rb +54 -23
- data/lib/paytree/version.rb +1 -1
- data/lib/paytree.rb +3 -2
- data/paytree.gemspec +2 -2
- metadata +7 -5
- data/paytree-0.2.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00f6d817e282de2ec9b8b94a6ff632ddb957b2c52f67cefe89a7afbf7dd6aee9
|
|
4
|
+
data.tar.gz: 9815218a37b228ee2e5b75e85bf3a7d0378b47907436df845a9163f6ee303de1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db7d90e5c0564309bb45394c66738f5e932172012c4f054417a2f7c357a08d5a7eb6dd2ce1b5bda6f8d32d80c044bc91dee633c14bab92475a2427466ed8d8b3
|
|
7
|
+
data.tar.gz: a9d0a1b6a51500501d8737edee17d7d56f10d21eab22135bd7a37a93fb49efe954f352106e0093937436daa0b8e6458499b2a635270bc1c90b88612b9510ec31
|
data/Gemfile.lock
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
paytree (0.
|
|
5
|
-
|
|
4
|
+
paytree (0.4.0)
|
|
5
|
+
httpx (~> 1.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activesupport (8.0.
|
|
10
|
+
activesupport (8.0.3)
|
|
11
11
|
base64
|
|
12
12
|
benchmark (>= 0.3)
|
|
13
13
|
bigdecimal
|
|
@@ -25,23 +25,20 @@ GEM
|
|
|
25
25
|
ast (2.4.3)
|
|
26
26
|
base64 (0.3.0)
|
|
27
27
|
benchmark (0.4.1)
|
|
28
|
-
bigdecimal (3.
|
|
28
|
+
bigdecimal (3.3.1)
|
|
29
29
|
concurrent-ruby (1.3.5)
|
|
30
|
-
connection_pool (2.5.
|
|
30
|
+
connection_pool (2.5.4)
|
|
31
31
|
crack (1.0.0)
|
|
32
32
|
bigdecimal
|
|
33
33
|
rexml
|
|
34
34
|
date (3.4.1)
|
|
35
35
|
diff-lcs (1.6.2)
|
|
36
36
|
drb (2.2.3)
|
|
37
|
-
erb (5.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
faraday-net_http (3.4.1)
|
|
43
|
-
net-http (>= 0.5.0)
|
|
44
|
-
hashdiff (1.2.0)
|
|
37
|
+
erb (5.1.1)
|
|
38
|
+
hashdiff (1.2.1)
|
|
39
|
+
http-2 (1.1.1)
|
|
40
|
+
httpx (1.6.2)
|
|
41
|
+
http-2 (>= 1.0.0)
|
|
45
42
|
i18n (1.14.7)
|
|
46
43
|
concurrent-ruby (~> 1.0)
|
|
47
44
|
io-console (0.8.1)
|
|
@@ -49,36 +46,35 @@ GEM
|
|
|
49
46
|
pp (>= 0.6.0)
|
|
50
47
|
rdoc (>= 4.0.0)
|
|
51
48
|
reline (>= 0.4.2)
|
|
52
|
-
json (2.
|
|
49
|
+
json (2.15.1)
|
|
53
50
|
language_server-protocol (3.17.0.5)
|
|
54
51
|
lint_roller (1.1.0)
|
|
55
52
|
logger (1.7.0)
|
|
56
|
-
minitest (5.
|
|
57
|
-
net-http (0.6.0)
|
|
58
|
-
uri
|
|
53
|
+
minitest (5.26.0)
|
|
59
54
|
parallel (1.27.0)
|
|
60
|
-
parser (3.3.
|
|
55
|
+
parser (3.3.9.0)
|
|
61
56
|
ast (~> 2.4.1)
|
|
62
57
|
racc
|
|
63
|
-
pp (0.6.
|
|
58
|
+
pp (0.6.3)
|
|
64
59
|
prettyprint
|
|
65
60
|
prettyprint (0.2.0)
|
|
66
|
-
prism (1.
|
|
61
|
+
prism (1.6.0)
|
|
67
62
|
psych (5.2.6)
|
|
68
63
|
date
|
|
69
64
|
stringio
|
|
70
65
|
public_suffix (6.0.2)
|
|
71
66
|
racc (1.8.1)
|
|
72
|
-
rack (3.
|
|
67
|
+
rack (3.2.3)
|
|
73
68
|
rainbow (3.1.1)
|
|
74
69
|
rake (13.3.0)
|
|
75
|
-
rdoc (6.
|
|
70
|
+
rdoc (6.15.0)
|
|
76
71
|
erb
|
|
77
72
|
psych (>= 4.0.0)
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
tsort
|
|
74
|
+
regexp_parser (2.11.3)
|
|
75
|
+
reline (0.6.2)
|
|
80
76
|
io-console (~> 0.5)
|
|
81
|
-
rexml (3.4.
|
|
77
|
+
rexml (3.4.4)
|
|
82
78
|
rspec (3.13.1)
|
|
83
79
|
rspec-core (~> 3.13.0)
|
|
84
80
|
rspec-expectations (~> 3.13.0)
|
|
@@ -88,11 +84,11 @@ GEM
|
|
|
88
84
|
rspec-expectations (3.13.5)
|
|
89
85
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
90
86
|
rspec-support (~> 3.13.0)
|
|
91
|
-
rspec-mocks (3.13.
|
|
87
|
+
rspec-mocks (3.13.6)
|
|
92
88
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
93
89
|
rspec-support (~> 3.13.0)
|
|
94
|
-
rspec-support (3.13.
|
|
95
|
-
rubocop (1.
|
|
90
|
+
rspec-support (3.13.6)
|
|
91
|
+
rubocop (1.80.2)
|
|
96
92
|
json (~> 2.3)
|
|
97
93
|
language_server-protocol (~> 3.17.0.2)
|
|
98
94
|
lint_roller (~> 1.1.0)
|
|
@@ -100,17 +96,17 @@ GEM
|
|
|
100
96
|
parser (>= 3.3.0.2)
|
|
101
97
|
rainbow (>= 2.2.2, < 4.0)
|
|
102
98
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
103
|
-
rubocop-ast (>= 1.
|
|
99
|
+
rubocop-ast (>= 1.46.0, < 2.0)
|
|
104
100
|
ruby-progressbar (~> 1.7)
|
|
105
101
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
106
|
-
rubocop-ast (1.
|
|
102
|
+
rubocop-ast (1.47.1)
|
|
107
103
|
parser (>= 3.3.7.2)
|
|
108
104
|
prism (~> 1.4)
|
|
109
105
|
rubocop-performance (1.25.0)
|
|
110
106
|
lint_roller (~> 1.1)
|
|
111
107
|
rubocop (>= 1.75.0, < 2.0)
|
|
112
108
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
113
|
-
rubocop-rails (2.
|
|
109
|
+
rubocop-rails (2.33.4)
|
|
114
110
|
activesupport (>= 4.2.0)
|
|
115
111
|
lint_roller (~> 1.1)
|
|
116
112
|
rack (>= 1.1)
|
|
@@ -122,10 +118,10 @@ GEM
|
|
|
122
118
|
rubocop-rails (>= 2.30)
|
|
123
119
|
ruby-progressbar (1.13.0)
|
|
124
120
|
securerandom (0.4.1)
|
|
125
|
-
standard (1.
|
|
121
|
+
standard (1.51.1)
|
|
126
122
|
language_server-protocol (~> 3.17.0.2)
|
|
127
123
|
lint_roller (~> 1.0)
|
|
128
|
-
rubocop (~> 1.
|
|
124
|
+
rubocop (~> 1.80.2)
|
|
129
125
|
standard-custom (~> 1.0.0)
|
|
130
126
|
standard-performance (~> 1.8)
|
|
131
127
|
standard-custom (1.0.2)
|
|
@@ -135,19 +131,22 @@ GEM
|
|
|
135
131
|
lint_roller (~> 1.1)
|
|
136
132
|
rubocop-performance (~> 1.25.0)
|
|
137
133
|
stringio (3.1.7)
|
|
134
|
+
tsort (0.2.0)
|
|
138
135
|
tzinfo (2.0.6)
|
|
139
136
|
concurrent-ruby (~> 1.0)
|
|
140
|
-
unicode-display_width (3.
|
|
141
|
-
unicode-emoji (~> 4.
|
|
142
|
-
unicode-emoji (4.0
|
|
143
|
-
uri (1.0.
|
|
137
|
+
unicode-display_width (3.2.0)
|
|
138
|
+
unicode-emoji (~> 4.1)
|
|
139
|
+
unicode-emoji (4.1.0)
|
|
140
|
+
uri (1.0.4)
|
|
144
141
|
webmock (3.25.1)
|
|
145
142
|
addressable (>= 2.8.0)
|
|
146
143
|
crack (>= 0.3.2)
|
|
147
144
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
148
145
|
|
|
149
146
|
PLATFORMS
|
|
147
|
+
arm64-darwin-25
|
|
150
148
|
x86_64-darwin-24
|
|
149
|
+
x86_64-darwin-25
|
|
151
150
|
x86_64-linux
|
|
152
151
|
|
|
153
152
|
DEPENDENCIES
|
data/README.md
CHANGED
|
@@ -18,6 +18,8 @@ Currently supports Kenya's M-Pesa via the Daraja API with plans for additional p
|
|
|
18
18
|
- **Convention over Configuration**: One clear setup pattern, opinionated defaults
|
|
19
19
|
- **Safe Defaults**: Sandbox mode, proper timeouts, comprehensive error handling
|
|
20
20
|
- **Batteries Included**: STK Push, B2C, B2B, C2B operations out of the box
|
|
21
|
+
- **API Versioning**: Support for both Daraja API v1 and v3 with backward compatibility
|
|
22
|
+
- **Enhanced Reliability**: Automatic token retry with exponential backoff
|
|
21
23
|
- **Security First**: Credential management, no hardcoded secrets
|
|
22
24
|
|
|
23
25
|
## Quick Start
|
|
@@ -89,12 +91,59 @@ Paytree.configure_mpesa(
|
|
|
89
91
|
# shortcode: "YOUR_PRODUCTION_SHORTCODE",
|
|
90
92
|
# passkey: Rails.application.credentials.mpesa[:passkey],
|
|
91
93
|
# sandbox: false,
|
|
92
|
-
#
|
|
94
|
+
# api_version: "v1", # Optional: "v1" (default) or "v3"
|
|
95
|
+
# retryable_errors: ["429.001.01", "500.001.02", "503.001.01"] # Optional: errors to retry
|
|
93
96
|
# )
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
---
|
|
97
100
|
|
|
101
|
+
## API Version Support
|
|
102
|
+
|
|
103
|
+
Paytree supports both M-Pesa Daraja API v1 and v3 endpoints. The API version can be configured globally or via environment variables.
|
|
104
|
+
|
|
105
|
+
### Configuration Options
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# Use v1 API (default - backward compatible)
|
|
109
|
+
Paytree.configure_mpesa(
|
|
110
|
+
key: "YOUR_KEY",
|
|
111
|
+
secret: "YOUR_SECRET",
|
|
112
|
+
api_version: "v1" # Default
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Use v3 API (latest features)
|
|
116
|
+
Paytree.configure_mpesa(
|
|
117
|
+
key: "YOUR_KEY",
|
|
118
|
+
secret: "YOUR_SECRET",
|
|
119
|
+
api_version: "v3"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Or via environment variable
|
|
123
|
+
ENV['MPESA_API_VERSION'] = 'v3'
|
|
124
|
+
Paytree.configure_mpesa(
|
|
125
|
+
key: "YOUR_KEY",
|
|
126
|
+
secret: "YOUR_SECRET"
|
|
127
|
+
# api_version automatically picked up from ENV
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Differences Between v1 and v3
|
|
132
|
+
|
|
133
|
+
| Feature | v1 | v3 |
|
|
134
|
+
|---------|----|----|
|
|
135
|
+
| **Endpoints** | `/mpesa/b2c/v1/paymentrequest` | `/mpesa/b2c/v3/paymentrequest` |
|
|
136
|
+
| **OriginatorConversationID** | Not required | Auto-generated UUID |
|
|
137
|
+
| **Reliability** | Standard | Enhanced with better tracking |
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
**Backward Compatibility:**
|
|
141
|
+
- Existing code continues to work unchanged (defaults to v1)
|
|
142
|
+
- No breaking changes when upgrading Paytree versions
|
|
143
|
+
- Can switch between v1/v3 by changing configuration only
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
98
147
|
## Usage Examples
|
|
99
148
|
|
|
100
149
|
### STK Push (Customer Payment)
|
|
@@ -168,7 +217,7 @@ end
|
|
|
168
217
|
|
|
169
218
|
Send funds directly to a customer’s M-Pesa wallet via the B2C API.
|
|
170
219
|
|
|
171
|
-
### Example
|
|
220
|
+
### Basic Example
|
|
172
221
|
```ruby
|
|
173
222
|
response = Paytree::Mpesa::B2C.call(
|
|
174
223
|
phone_number: "254712345678",
|
|
@@ -186,6 +235,43 @@ else
|
|
|
186
235
|
end
|
|
187
236
|
```
|
|
188
237
|
|
|
238
|
+
### v3 API Features
|
|
239
|
+
|
|
240
|
+
When using `api_version: "v3"`, B2C calls automatically include an `OriginatorConversationID` for enhanced tracking:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# Configure for v3 API
|
|
244
|
+
Paytree.configure_mpesa(
|
|
245
|
+
key: "YOUR_KEY",
|
|
246
|
+
secret: "YOUR_SECRET",
|
|
247
|
+
api_version: "v3"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Same call, but now uses v3 endpoint with auto-generated OriginatorConversationID
|
|
251
|
+
response = Paytree::Mpesa::B2C.call(
|
|
252
|
+
phone_number: "254712345678",
|
|
253
|
+
amount: 100
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# v3 response includes additional tracking data
|
|
257
|
+
if response.success?
|
|
258
|
+
puts "Conversation ID: #{response.data["ConversationID"]}"
|
|
259
|
+
puts "Originator ID: #{response.data["OriginatorConversationID"]}" # Auto-generated UUID
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Custom OriginatorConversationID (v3 only)
|
|
264
|
+
|
|
265
|
+
You can provide your own tracking ID for v3 API calls:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
response = Paytree::Mpesa::B2C.call(
|
|
269
|
+
phone_number: "254712345678",
|
|
270
|
+
amount: 100,
|
|
271
|
+
originator_conversation_id: "CUSTOM-TRACK-#{Time.now.to_i}"
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
189
275
|
---
|
|
190
276
|
|
|
191
277
|
## C2B (Customer to Business)
|
|
@@ -321,6 +407,9 @@ Paytree allows you to configure which error codes should be considered retryable
|
|
|
321
407
|
- `"429.001.01"` - Rate limit exceeded
|
|
322
408
|
- `"500.001.02"` - Temporary server error
|
|
323
409
|
- `"503.001.01"` - Service temporarily unavailable
|
|
410
|
+
- `"timeout.connection"` - Network connection timeout (Net::OpenTimeout)
|
|
411
|
+
- `"timeout.read"` - Network read timeout (Net::ReadTimeout)
|
|
412
|
+
- `"timeout.request"` - HTTP request timeout (HTTPX::TimeoutError)
|
|
324
413
|
|
|
325
414
|
Configure retryable errors during setup:
|
|
326
415
|
|
|
@@ -328,7 +417,14 @@ Configure retryable errors during setup:
|
|
|
328
417
|
Paytree.configure_mpesa(
|
|
329
418
|
key: "YOUR_KEY",
|
|
330
419
|
secret: "YOUR_SECRET",
|
|
331
|
-
retryable_errors: [
|
|
420
|
+
retryable_errors: [
|
|
421
|
+
"429.001.01", # Rate limit
|
|
422
|
+
"500.001.02", # Server error
|
|
423
|
+
"503.001.01", # Service unavailable
|
|
424
|
+
"timeout.connection", # Connection timeout
|
|
425
|
+
"timeout.read", # Read timeout
|
|
426
|
+
"timeout.request" # Request timeout
|
|
427
|
+
]
|
|
332
428
|
)
|
|
333
429
|
```
|
|
334
430
|
|
|
@@ -3,16 +3,17 @@ require "logger"
|
|
|
3
3
|
module Paytree
|
|
4
4
|
module Configs
|
|
5
5
|
class Mpesa
|
|
6
|
+
attr_writer :logger
|
|
6
7
|
attr_accessor :key, :secret, :shortcode, :passkey, :adapter,
|
|
7
8
|
:initiator_name, :initiator_password, :sandbox,
|
|
8
|
-
:extras, :timeout, :retryable_errors
|
|
9
|
+
:extras, :timeout, :retryable_errors, :api_version
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@extras = {}
|
|
12
13
|
@logger = nil
|
|
13
|
-
@mutex = Mutex.new
|
|
14
14
|
@timeout = 30 # Default 30 second timeout
|
|
15
15
|
@retryable_errors = [] # Default empty array
|
|
16
|
+
@api_version = "v1" # Default to v1 for backward compatibility
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def base_url
|
|
@@ -20,15 +21,7 @@ module Paytree
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def logger
|
|
23
|
-
@
|
|
24
|
-
@logger ||= Logger.new($stdout)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def logger=(new_logger)
|
|
29
|
-
@mutex.synchronize do
|
|
30
|
-
@logger = new_logger
|
|
31
|
-
end
|
|
24
|
+
@logger ||= Logger.new($stdout)
|
|
32
25
|
end
|
|
33
26
|
end
|
|
34
27
|
end
|
|
@@ -5,7 +5,9 @@ module Paytree
|
|
|
5
5
|
module Adapters
|
|
6
6
|
module Daraja
|
|
7
7
|
class B2B < Base
|
|
8
|
-
|
|
8
|
+
def self.endpoint
|
|
9
|
+
"/mpesa/b2b/#{config.api_version}/paymentrequest"
|
|
10
|
+
end
|
|
9
11
|
|
|
10
12
|
class << self
|
|
11
13
|
def call(short_code:, receiver_shortcode:, amount:, account_reference:, **opts)
|
|
@@ -28,7 +30,7 @@ module Paytree
|
|
|
28
30
|
ResultURL: config.extras[:result_url]
|
|
29
31
|
}.compact
|
|
30
32
|
|
|
31
|
-
post_to_mpesa(:b2b,
|
|
33
|
+
post_to_mpesa(:b2b, endpoint, payload)
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
|
@@ -5,7 +5,9 @@ module Paytree
|
|
|
5
5
|
module Adapters
|
|
6
6
|
module Daraja
|
|
7
7
|
class B2C < Base
|
|
8
|
-
|
|
8
|
+
def self.endpoint
|
|
9
|
+
"/mpesa/b2c/#{config.api_version}/paymentrequest"
|
|
10
|
+
end
|
|
9
11
|
|
|
10
12
|
class << self
|
|
11
13
|
def call(phone_number:, amount:, **opts)
|
|
@@ -23,9 +25,14 @@ module Paytree
|
|
|
23
25
|
CommandID: opts[:command_id] || "BusinessPayment",
|
|
24
26
|
Remarks: opts[:remarks] || "OK",
|
|
25
27
|
Occasion: opts[:occasion] || "Payment"
|
|
26
|
-
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Add OriginatorConversationID for v3
|
|
31
|
+
if config.api_version == "v3"
|
|
32
|
+
payload[:OriginatorConversationID] = opts[:originator_conversation_id] || generate_conversation_id
|
|
33
|
+
end
|
|
27
34
|
|
|
28
|
-
post_to_mpesa(:b2c,
|
|
35
|
+
post_to_mpesa(:b2c, endpoint, payload.compact)
|
|
29
36
|
end
|
|
30
37
|
end
|
|
31
38
|
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
require "base64"
|
|
2
|
+
require "securerandom"
|
|
2
3
|
require_relative "response_helpers"
|
|
4
|
+
require_relative "http_client_factory"
|
|
5
|
+
require_relative "validator"
|
|
6
|
+
require_relative "token_manager"
|
|
3
7
|
require_relative "../../../utils/error_handling"
|
|
4
8
|
|
|
5
9
|
module Paytree
|
|
@@ -10,36 +14,32 @@ module Paytree
|
|
|
10
14
|
class << self
|
|
11
15
|
include Paytree::Utils::ErrorHandling
|
|
12
16
|
include Paytree::Mpesa::Adapters::Daraja::ResponseHelpers
|
|
17
|
+
include Paytree::Mpesa::Adapters::Daraja::HttpClientFactory
|
|
18
|
+
include Paytree::Mpesa::Adapters::Daraja::Validator
|
|
19
|
+
include Paytree::Mpesa::Adapters::Daraja::TokenManager
|
|
13
20
|
|
|
14
21
|
def config = Paytree[:mpesa]
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
# Thread-safe HTTP client for regular API calls
|
|
24
|
+
def http_client
|
|
25
|
+
thread_safe_client(:@http_client)
|
|
26
|
+
end
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
# Thread-safe HTTP client with retry logic for token fetching
|
|
29
|
+
# Retries on: timeouts, connection errors, and 5xx server errors
|
|
30
|
+
def token_http_client
|
|
31
|
+
thread_safe_client(:@token_http_client, plugins: [:retries], **retry_options)
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
def post_to_mpesa(operation, endpoint, payload)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
operation
|
|
30
|
-
)
|
|
35
|
+
response = http_client.post(endpoint, json: payload, headers:)
|
|
36
|
+
build_response(response, operation)
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
def headers
|
|
34
40
|
{"Authorization" => "Bearer #{token}", "Content-Type" => "application/json"}
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
def token
|
|
38
|
-
return @token if token_valid?
|
|
39
|
-
|
|
40
|
-
fetch_token
|
|
41
|
-
end
|
|
42
|
-
|
|
43
43
|
def encrypt_credential(config)
|
|
44
44
|
cert_path = config.extras[:cert_path]
|
|
45
45
|
unless cert_path && File.exist?(cert_path)
|
|
@@ -55,76 +55,8 @@ module Paytree
|
|
|
55
55
|
"Failed to encrypt password with certificate #{cert_path}: #{e.message}"
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# ------------------------------------------------------------------
|
|
61
|
-
VALIDATIONS = {
|
|
62
|
-
c2b_register: {required: %i[short_code confirmation_url validation_url]},
|
|
63
|
-
c2b_simulate: {required: %i[phone_number amount reference]},
|
|
64
|
-
stk_push: {required: %i[phone_number amount reference]},
|
|
65
|
-
b2c: {required: %i[phone_number amount], config: %i[result_url]},
|
|
66
|
-
b2b: {
|
|
67
|
-
required: %i[short_code receiver_shortcode account_reference amount],
|
|
68
|
-
config: %i[result_url timeout_url],
|
|
69
|
-
command_id: %w[BusinessPayBill BusinessBuyGoods]
|
|
70
|
-
}
|
|
71
|
-
}.freeze
|
|
72
|
-
|
|
73
|
-
def validate_for(operation, params = {})
|
|
74
|
-
rules = VALIDATIONS[operation] ||
|
|
75
|
-
raise(Paytree::Errors::UnsupportedOperation, "Unknown operation: #{operation}")
|
|
76
|
-
|
|
77
|
-
Array(rules[:required]).each { |field| validate_field(field, params[field]) }
|
|
78
|
-
|
|
79
|
-
Array(rules[:config]).each do |key|
|
|
80
|
-
unless config.extras[key]
|
|
81
|
-
raise Paytree::Errors::ConfigurationError, "Missing `#{key}` in Mpesa extras config"
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
if (allowed = rules[:command_id]) && !allowed.include?(params[:command_id])
|
|
86
|
-
raise Paytree::Errors::ValidationError,
|
|
87
|
-
"command_id must be one of: #{allowed.join(", ")}"
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def validate_field(field, value)
|
|
92
|
-
case field
|
|
93
|
-
when :amount
|
|
94
|
-
unless value.is_a?(Numeric) && value >= 1
|
|
95
|
-
raise Paytree::Errors::ValidationError,
|
|
96
|
-
"amount must be a positive number"
|
|
97
|
-
end
|
|
98
|
-
when :phone_number
|
|
99
|
-
phone_regex = /^254\d{9}$/
|
|
100
|
-
unless value.to_s.match?(phone_regex)
|
|
101
|
-
raise Paytree::Errors::ValidationError,
|
|
102
|
-
"phone_number must be a valid Kenyan format (254XXXXXXXXX)"
|
|
103
|
-
end
|
|
104
|
-
else
|
|
105
|
-
raise Paytree::Errors::ValidationError, "#{field} cannot be blank" if value.to_s.strip.empty?
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
def fetch_token
|
|
112
|
-
cred = Base64.strict_encode64("#{config.key}:#{config.secret}")
|
|
113
|
-
|
|
114
|
-
response = connection.get("/oauth/v1/generate", grant_type: "client_credentials") do |r|
|
|
115
|
-
r.headers["Authorization"] = "Basic #{cred}"
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
data = response.body
|
|
119
|
-
@token = data["access_token"]
|
|
120
|
-
@token_expiry = Time.now + data["expires_in"].to_i
|
|
121
|
-
@token
|
|
122
|
-
rescue Faraday::Error => e
|
|
123
|
-
raise Paytree::Errors::MpesaTokenError, "Unable to fetch token: #{e.message}"
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def token_valid?
|
|
127
|
-
@token && @token_expiry && Time.now < @token_expiry
|
|
58
|
+
def generate_conversation_id
|
|
59
|
+
SecureRandom.uuid
|
|
128
60
|
end
|
|
129
61
|
end
|
|
130
62
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Paytree
|
|
2
|
+
module Mpesa
|
|
3
|
+
module Adapters
|
|
4
|
+
module Daraja
|
|
5
|
+
module HttpClientFactory
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Creates a thread-safe memoized HTTP client
|
|
9
|
+
# @param ivar_name [Symbol] Instance variable name (e.g., :@http_client)
|
|
10
|
+
# @param plugins [Array<Symbol>] HTTPX plugins to load (e.g., [:retries])
|
|
11
|
+
# @param options [Hash] Additional HTTPX options
|
|
12
|
+
# @return [HTTPX::Session] Configured HTTP client
|
|
13
|
+
def thread_safe_client(ivar_name, plugins: [], **options)
|
|
14
|
+
cached = instance_variable_get(ivar_name)
|
|
15
|
+
return cached if cached
|
|
16
|
+
|
|
17
|
+
mutex_name = :"#{ivar_name}_mutex"
|
|
18
|
+
mutex = instance_variable_get(mutex_name) || instance_variable_set(mutex_name, Mutex.new)
|
|
19
|
+
|
|
20
|
+
mutex.synchronize do
|
|
21
|
+
cached = instance_variable_get(ivar_name)
|
|
22
|
+
return cached if cached
|
|
23
|
+
|
|
24
|
+
client = plugins.reduce(HTTPX) { |c, plugin| c.plugin(plugin) }
|
|
25
|
+
|
|
26
|
+
instance_variable_set(
|
|
27
|
+
ivar_name,
|
|
28
|
+
client.with(base_http_options.merge(options))
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def base_http_options
|
|
34
|
+
{
|
|
35
|
+
origin: config.base_url,
|
|
36
|
+
timeout: {
|
|
37
|
+
connect_timeout: config.timeout / 2,
|
|
38
|
+
operation_timeout: config.timeout
|
|
39
|
+
},
|
|
40
|
+
ssl: {
|
|
41
|
+
verify_mode: OpenSSL::SSL::VERIFY_NONE
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def retry_options
|
|
47
|
+
{
|
|
48
|
+
max_retries: 3,
|
|
49
|
+
retry_change_requests: true,
|
|
50
|
+
retry_on: ->(response) {
|
|
51
|
+
# Retry on network errors (timeouts, connection failures)
|
|
52
|
+
return true if response.is_a?(HTTPX::ErrorResponse)
|
|
53
|
+
# Retry on 5xx server errors
|
|
54
|
+
[500, 502, 503, 504].include?(response.status)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -4,12 +4,18 @@ module Paytree
|
|
|
4
4
|
module Daraja
|
|
5
5
|
module ResponseHelpers
|
|
6
6
|
def build_response(response, operation)
|
|
7
|
-
|
|
7
|
+
# Handle ErrorResponse from HTTPX
|
|
8
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
|
9
|
+
raise response.error if response.error
|
|
10
|
+
raise HTTPX::Error, "Request failed"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
parsed = response.json
|
|
8
14
|
|
|
9
15
|
Paytree::Response.new(
|
|
10
16
|
provider: :mpesa,
|
|
11
17
|
operation:,
|
|
12
|
-
status: response
|
|
18
|
+
status: successful_response?(response) ? :success : :error,
|
|
13
19
|
message: response_message(parsed),
|
|
14
20
|
code: response_code(parsed),
|
|
15
21
|
data: parsed,
|
|
@@ -19,6 +25,10 @@ module Paytree
|
|
|
19
25
|
|
|
20
26
|
private
|
|
21
27
|
|
|
28
|
+
def successful_response?(response)
|
|
29
|
+
response.status >= 200 && response.status < 300
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
def response_message(parsed)
|
|
23
33
|
parsed["ResponseDescription"] ||
|
|
24
34
|
parsed["ResultDesc"] ||
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
|
|
3
|
+
module Paytree
|
|
4
|
+
module Mpesa
|
|
5
|
+
module Adapters
|
|
6
|
+
module Daraja
|
|
7
|
+
# Thread-safe token management for M-Pesa Daraja API
|
|
8
|
+
# Handles OAuth token fetching, caching, and expiry validation
|
|
9
|
+
module TokenManager
|
|
10
|
+
# Returns a valid access token, fetching a new one if needed
|
|
11
|
+
# @return [String] Valid access token
|
|
12
|
+
def token
|
|
13
|
+
return @token if token_valid?
|
|
14
|
+
|
|
15
|
+
@token_mutex ||= Mutex.new
|
|
16
|
+
@token_mutex.synchronize do
|
|
17
|
+
return @token if token_valid?
|
|
18
|
+
|
|
19
|
+
fetch_token
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Fetches a new OAuth token from M-Pesa API
|
|
26
|
+
# Automatically retries on network errors and 5xx responses
|
|
27
|
+
# @return [String] Access token
|
|
28
|
+
# @raise [Paytree::Errors::MpesaTokenError] if token fetch fails
|
|
29
|
+
def fetch_token
|
|
30
|
+
credentials = encode_credentials
|
|
31
|
+
|
|
32
|
+
response = token_http_client.get(
|
|
33
|
+
"/oauth/v1/generate",
|
|
34
|
+
params: {grant_type: "client_credentials"},
|
|
35
|
+
headers: {"Authorization" => "Basic #{credentials}"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
validate_token_response(response)
|
|
39
|
+
parse_and_cache_token(response)
|
|
40
|
+
rescue HTTPX::Error => e
|
|
41
|
+
raise Paytree::Errors::MpesaTokenError, "Unable to fetch token: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def encode_credentials
|
|
45
|
+
Base64.strict_encode64("#{config.key}:#{config.secret}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_token_response(response)
|
|
49
|
+
if response.is_a?(HTTPX::ErrorResponse) || response.error
|
|
50
|
+
error_msg = response.error ? response.error.message : "Request failed"
|
|
51
|
+
raise Paytree::Errors::MpesaTokenError, "Unable to fetch token: #{error_msg}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
unless response.status == 200
|
|
55
|
+
raise Paytree::Errors::MpesaTokenError,
|
|
56
|
+
"Token request failed with status #{response.status}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_and_cache_token(response)
|
|
61
|
+
response_data = response.json
|
|
62
|
+
@token = response_data["access_token"]
|
|
63
|
+
@token_expiry = Time.now + response_data["expires_in"].to_i
|
|
64
|
+
@token
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def token_valid?
|
|
68
|
+
@token && @token_expiry && Time.now < @token_expiry
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module Paytree
|
|
2
|
+
module Mpesa
|
|
3
|
+
module Adapters
|
|
4
|
+
module Daraja
|
|
5
|
+
# Validation module for M-Pesa API operations
|
|
6
|
+
# Validates required fields, config values, and operation-specific parameters
|
|
7
|
+
module Validator
|
|
8
|
+
VALIDATIONS = {
|
|
9
|
+
c2b_register: {required: %i[short_code confirmation_url validation_url]},
|
|
10
|
+
c2b_simulate: {required: %i[phone_number amount reference]},
|
|
11
|
+
stk_push: {required: %i[phone_number amount reference]},
|
|
12
|
+
b2c: {required: %i[phone_number amount], config: %i[result_url]},
|
|
13
|
+
b2b: {
|
|
14
|
+
required: %i[short_code receiver_shortcode account_reference amount],
|
|
15
|
+
config: %i[result_url timeout_url],
|
|
16
|
+
command_id: %w[BusinessPayBill BusinessBuyGoods]
|
|
17
|
+
}
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Validates parameters for a given operation
|
|
21
|
+
# @param operation [Symbol] The operation to validate for (e.g., :stk_push, :b2c)
|
|
22
|
+
# @param params [Hash] Parameters to validate
|
|
23
|
+
# @raise [Paytree::Errors::UnsupportedOperation] if operation is unknown
|
|
24
|
+
# @raise [Paytree::Errors::ValidationError] if validation fails
|
|
25
|
+
# @raise [Paytree::Errors::ConfigurationError] if required config is missing
|
|
26
|
+
def validate_for(operation, params = {})
|
|
27
|
+
rules = VALIDATIONS[operation] ||
|
|
28
|
+
raise(Paytree::Errors::UnsupportedOperation, "Unknown operation: #{operation}")
|
|
29
|
+
|
|
30
|
+
# Validate required fields
|
|
31
|
+
Array(rules[:required]).each { |field| validate_field(field, params[field]) }
|
|
32
|
+
|
|
33
|
+
# Validate required config values
|
|
34
|
+
Array(rules[:config]).each do |key|
|
|
35
|
+
unless config.extras[key]
|
|
36
|
+
raise Paytree::Errors::ConfigurationError, "Missing `#{key}` in Mpesa extras config"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validate command_id if specified
|
|
41
|
+
if (allowed = rules[:command_id]) && !allowed.include?(params[:command_id])
|
|
42
|
+
raise Paytree::Errors::ValidationError,
|
|
43
|
+
"command_id must be one of: #{allowed.join(", ")}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validates a single field based on its type
|
|
48
|
+
# @param field [Symbol] Field name
|
|
49
|
+
# @param value [Object] Field value to validate
|
|
50
|
+
# @raise [Paytree::Errors::ValidationError] if validation fails
|
|
51
|
+
def validate_field(field, value)
|
|
52
|
+
case field
|
|
53
|
+
when :amount then validate_amount(value)
|
|
54
|
+
when :phone_number then validate_phone_number(value)
|
|
55
|
+
else
|
|
56
|
+
validate_presence(field, value)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Validates amount is a positive number
|
|
63
|
+
def validate_amount(value)
|
|
64
|
+
unless value.is_a?(Numeric) && value >= 1
|
|
65
|
+
raise Paytree::Errors::ValidationError,
|
|
66
|
+
"amount must be a positive number"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Validates phone number is in valid Kenyan format (254XXXXXXXXX)
|
|
71
|
+
def validate_phone_number(value)
|
|
72
|
+
phone_regex = /^254\d{9}$/
|
|
73
|
+
unless value.to_s.match?(phone_regex)
|
|
74
|
+
raise Paytree::Errors::ValidationError,
|
|
75
|
+
"phone_number must be a valid Kenyan format (254XXXXXXXXX)"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Validates field is not blank
|
|
80
|
+
def validate_presence(field, value)
|
|
81
|
+
if value.to_s.strip.empty?
|
|
82
|
+
raise Paytree::Errors::ValidationError, "#{field} cannot be blank"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -7,35 +7,61 @@ module Paytree
|
|
|
7
7
|
Paytree::Errors::Base => e
|
|
8
8
|
emit_error(e, context)
|
|
9
9
|
raise
|
|
10
|
-
rescue
|
|
11
|
-
|
|
10
|
+
rescue HTTPX::TimeoutError => e
|
|
11
|
+
handle_http_error(
|
|
12
12
|
e,
|
|
13
13
|
context,
|
|
14
14
|
error_class: Paytree::Errors::MpesaResponseError,
|
|
15
|
-
error_type: "Timeout"
|
|
15
|
+
error_type: "Timeout",
|
|
16
|
+
code: "timeout.request"
|
|
16
17
|
)
|
|
17
|
-
rescue
|
|
18
|
-
|
|
18
|
+
rescue Net::OpenTimeout => e
|
|
19
|
+
handle_http_error(
|
|
19
20
|
e,
|
|
20
21
|
context,
|
|
21
|
-
error_class: Paytree::Errors::
|
|
22
|
-
error_type: "
|
|
22
|
+
error_class: Paytree::Errors::MpesaResponseError,
|
|
23
|
+
error_type: "Timeout",
|
|
24
|
+
code: "timeout.connection"
|
|
25
|
+
)
|
|
26
|
+
rescue Net::ReadTimeout => e
|
|
27
|
+
handle_http_error(
|
|
28
|
+
e,
|
|
29
|
+
context,
|
|
30
|
+
error_class: Paytree::Errors::MpesaResponseError,
|
|
31
|
+
error_type: "Timeout",
|
|
32
|
+
code: "timeout.read"
|
|
23
33
|
)
|
|
24
|
-
rescue
|
|
25
|
-
|
|
34
|
+
rescue JSON::ParserError => e
|
|
35
|
+
handle_http_error(
|
|
26
36
|
e,
|
|
27
37
|
context,
|
|
28
|
-
error_class: Paytree::Errors::
|
|
29
|
-
error_type: "
|
|
30
|
-
extract_info: true
|
|
38
|
+
error_class: Paytree::Errors::MpesaMalformedResponse,
|
|
39
|
+
error_type: "Malformed response"
|
|
31
40
|
)
|
|
32
|
-
rescue
|
|
33
|
-
|
|
41
|
+
rescue HTTPX::HTTPError => e
|
|
42
|
+
if e.response.status >= 500
|
|
43
|
+
handle_http_error(
|
|
44
|
+
e,
|
|
45
|
+
context,
|
|
46
|
+
error_class: Paytree::Errors::MpesaServerError,
|
|
47
|
+
error_type: "Server error",
|
|
48
|
+
extract_info: true
|
|
49
|
+
)
|
|
50
|
+
else
|
|
51
|
+
handle_http_error(
|
|
52
|
+
e,
|
|
53
|
+
context,
|
|
54
|
+
error_class: Paytree::Errors::MpesaClientError,
|
|
55
|
+
error_type: "Client error",
|
|
56
|
+
extract_info: true
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
rescue HTTPX::Error => e
|
|
60
|
+
handle_http_error(
|
|
34
61
|
e,
|
|
35
62
|
context,
|
|
36
|
-
error_class: Paytree::Errors::
|
|
37
|
-
error_type: "
|
|
38
|
-
extract_info: true
|
|
63
|
+
error_class: Paytree::Errors::MpesaResponseError,
|
|
64
|
+
error_type: "HTTP error"
|
|
39
65
|
)
|
|
40
66
|
rescue => e
|
|
41
67
|
wrap_and_raise(
|
|
@@ -47,14 +73,13 @@ module Paytree
|
|
|
47
73
|
|
|
48
74
|
private
|
|
49
75
|
|
|
50
|
-
def
|
|
76
|
+
def handle_http_error(error, context, error_class:, error_type:, extract_info: false, code: nil)
|
|
51
77
|
if extract_info
|
|
52
|
-
info =
|
|
78
|
+
info = parse_http_error(error)
|
|
53
79
|
message = info[:message] || error.message
|
|
54
|
-
code
|
|
80
|
+
code ||= info[:code]
|
|
55
81
|
else
|
|
56
82
|
message = error.message
|
|
57
|
-
code = nil
|
|
58
83
|
end
|
|
59
84
|
|
|
60
85
|
wrap_and_raise(
|
|
@@ -88,8 +113,14 @@ module Paytree
|
|
|
88
113
|
nil
|
|
89
114
|
end
|
|
90
115
|
|
|
91
|
-
def
|
|
92
|
-
|
|
116
|
+
def parse_http_error(http_error)
|
|
117
|
+
return {} unless http_error.respond_to?(:response)
|
|
118
|
+
|
|
119
|
+
body = begin
|
|
120
|
+
http_error.response.json
|
|
121
|
+
rescue
|
|
122
|
+
http_error.response.body.to_s
|
|
123
|
+
end
|
|
93
124
|
return {} unless body.is_a?(Hash)
|
|
94
125
|
|
|
95
126
|
{
|
data/lib/paytree/version.rb
CHANGED
data/lib/paytree.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
-
require "
|
|
2
|
+
require "httpx"
|
|
3
3
|
|
|
4
4
|
Dir[File.join(__dir__, "paytree/**/*.rb")].sort.each { |file| require file }
|
|
5
5
|
|
|
@@ -51,7 +51,8 @@ module Paytree
|
|
|
51
51
|
passkey: "MPESA_PASSKEY",
|
|
52
52
|
initiator_name: "MPESA_INITIATOR_NAME",
|
|
53
53
|
initiator_password: "MPESA_INITIATOR_PASSWORD",
|
|
54
|
-
sandbox: "MPESA_SANDBOX"
|
|
54
|
+
sandbox: "MPESA_SANDBOX",
|
|
55
|
+
api_version: "MPESA_API_VERSION"
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
config = {}
|
data/paytree.gemspec
CHANGED
|
@@ -27,14 +27,14 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
|
|
28
28
|
spec.files = Dir.chdir(__dir__) do
|
|
29
29
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
-
f.match(%r{^(test|spec|features|bin|exe)/}) || f.include?(".git")
|
|
30
|
+
f.match(%r{^(test|spec|features|bin|exe)/}) || f.include?(".git") || f.end_with?(".gem")
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
spec.require_paths = ["lib"]
|
|
35
35
|
|
|
36
36
|
# Runtime deps
|
|
37
|
-
spec.add_dependency "
|
|
37
|
+
spec.add_dependency "httpx", "~> 1.0"
|
|
38
38
|
|
|
39
39
|
# Dev/test deps
|
|
40
40
|
spec.add_development_dependency "rspec", "~> 3.12"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paytree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Charles Chuck
|
|
@@ -10,19 +10,19 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: httpx
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '1.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '1.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rspec
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -112,9 +112,12 @@ files:
|
|
|
112
112
|
- lib/paytree/mpesa/adapters/daraja/b2c.rb
|
|
113
113
|
- lib/paytree/mpesa/adapters/daraja/base.rb
|
|
114
114
|
- lib/paytree/mpesa/adapters/daraja/c2b.rb
|
|
115
|
+
- lib/paytree/mpesa/adapters/daraja/http_client_factory.rb
|
|
115
116
|
- lib/paytree/mpesa/adapters/daraja/response_helpers.rb
|
|
116
117
|
- lib/paytree/mpesa/adapters/daraja/stk_push.rb
|
|
117
118
|
- lib/paytree/mpesa/adapters/daraja/stk_query.rb
|
|
119
|
+
- lib/paytree/mpesa/adapters/daraja/token_manager.rb
|
|
120
|
+
- lib/paytree/mpesa/adapters/daraja/validator.rb
|
|
118
121
|
- lib/paytree/mpesa/b2b.rb
|
|
119
122
|
- lib/paytree/mpesa/b2c.rb
|
|
120
123
|
- lib/paytree/mpesa/c2b.rb
|
|
@@ -123,7 +126,6 @@ files:
|
|
|
123
126
|
- lib/paytree/response.rb
|
|
124
127
|
- lib/paytree/utils/error_handling.rb
|
|
125
128
|
- lib/paytree/version.rb
|
|
126
|
-
- paytree-0.2.0.gem
|
|
127
129
|
- paytree.gemspec
|
|
128
130
|
- sig/paytree.rbs
|
|
129
131
|
homepage: https://github.com/mundanecodes/paytree
|
data/paytree-0.2.0.gem
DELETED
|
Binary file
|