tinker-pay-ruby-sdk 0.1.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 +7 -0
- data/.rspec_status +31 -0
- data/.rubocop.yml +64 -0
- data/Gemfile +11 -0
- data/README.md +166 -0
- data/Rakefile +8 -0
- data/lib/tinker/api/base_manager.rb +47 -0
- data/lib/tinker/api/transaction_manager.rb +21 -0
- data/lib/tinker/auth/authentication_manager.rb +66 -0
- data/lib/tinker/config/configuration.rb +19 -0
- data/lib/tinker/config/endpoints.rb +13 -0
- data/lib/tinker/enum/gateway.rb +11 -0
- data/lib/tinker/enum/payment_status.rb +12 -0
- data/lib/tinker/exception/api_exception.rb +14 -0
- data/lib/tinker/exception/authentication_exception.rb +15 -0
- data/lib/tinker/exception/client_exception.rb +15 -0
- data/lib/tinker/exception/exception_code.rb +15 -0
- data/lib/tinker/exception/invalid_payload_exception.rb +15 -0
- data/lib/tinker/exception/network_exception.rb +15 -0
- data/lib/tinker/exception/webhook_exception.rb +15 -0
- data/lib/tinker/http/http_client.rb +48 -0
- data/lib/tinker/model/dto/callback_data_dto.rb +40 -0
- data/lib/tinker/model/dto/initiate_payment_request_dto.rb +44 -0
- data/lib/tinker/model/dto/initiation_data_dto.rb +30 -0
- data/lib/tinker/model/dto/query_data_dto.rb +40 -0
- data/lib/tinker/model/dto/query_payment_request_dto.rb +25 -0
- data/lib/tinker/model/transaction.rb +49 -0
- data/lib/tinker/payments.rb +34 -0
- data/lib/tinker/version.rb +5 -0
- data/lib/tinker/webhook/dto/invoice_event_data_dto.rb +35 -0
- data/lib/tinker/webhook/dto/payment_event_data_dto.rb +40 -0
- data/lib/tinker/webhook/dto/settlement_event_data_dto.rb +33 -0
- data/lib/tinker/webhook/dto/subscription_event_data_dto.rb +35 -0
- data/lib/tinker/webhook/webhook_event.rb +75 -0
- data/lib/tinker/webhook/webhook_handler.rb +35 -0
- data/lib/tinker/webhook/webhook_meta.rb +15 -0
- data/lib/tinker/webhook/webhook_security.rb +14 -0
- data/lib/tinker.rb +33 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b0b952007a71c15acfc412bf15ea914030bad28ff7d85fb947c64e81eb4e354d
|
|
4
|
+
data.tar.gz: 0c47365417e7520100310c3c87d9e0e72b28b7add64060a8a4c7bac300e93bff
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 827689281ecc69e936adf6edf3ec4e63d7e5425310dc7e47405c2062c4ab2f807abf946a0de279c78f7f1d5308b72db29c56f8b9bb35adf115374bd3179e45d0
|
|
7
|
+
data.tar.gz: 6c9bab9ab2168427ac3c183dfce37c3560c9fc646e8e9439c0dbe700a4cbd2c0f83ca5ac785d7074b63f606d3a539ed4488e1ba8112c9bcf638f1324d7622949
|
data/.rspec_status
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
example_id | status | run_time |
|
|
2
|
+
----------------------------------------------------- | ------ | --------------- |
|
|
3
|
+
./spec/integration/transaction_manager_spec.rb[1:1:1] | passed | 0.0268 seconds |
|
|
4
|
+
./spec/integration/transaction_manager_spec.rb[1:2:1] | passed | 0.00253 seconds |
|
|
5
|
+
./spec/unit/config/configuration_spec.rb[1:1:1] | passed | 0.00005 seconds |
|
|
6
|
+
./spec/unit/config/configuration_spec.rb[1:2:1] | passed | 0.00003 seconds |
|
|
7
|
+
./spec/unit/enum/gateway_spec.rb[1:1] | passed | 0.00003 seconds |
|
|
8
|
+
./spec/unit/enum/gateway_spec.rb[1:2] | passed | 0.00003 seconds |
|
|
9
|
+
./spec/unit/enum/gateway_spec.rb[1:3] | passed | 0.00003 seconds |
|
|
10
|
+
./spec/unit/enum/payment_status_spec.rb[1:1] | passed | 0.00003 seconds |
|
|
11
|
+
./spec/unit/enum/payment_status_spec.rb[1:2] | passed | 0.00002 seconds |
|
|
12
|
+
./spec/unit/enum/payment_status_spec.rb[1:3] | passed | 0.00032 seconds |
|
|
13
|
+
./spec/unit/enum/payment_status_spec.rb[1:4] | passed | 0.00073 seconds |
|
|
14
|
+
./spec/unit/model/transaction_spec.rb[1:1:1:1] | passed | 0.00155 seconds |
|
|
15
|
+
./spec/unit/model/transaction_spec.rb[1:1:2:1] | passed | 0.00012 seconds |
|
|
16
|
+
./spec/unit/model/transaction_spec.rb[1:2:1] | passed | 0.0002 seconds |
|
|
17
|
+
./spec/unit/model/transaction_spec.rb[1:3:1] | passed | 0.00005 seconds |
|
|
18
|
+
./spec/unit/model/transaction_spec.rb[1:4:1] | passed | 0.00009 seconds |
|
|
19
|
+
./spec/unit/model/transaction_spec.rb[1:5:1] | passed | 0.00008 seconds |
|
|
20
|
+
./spec/unit/tinker_payments_spec.rb[1:1:1] | passed | 0.00008 seconds |
|
|
21
|
+
./spec/unit/tinker_payments_spec.rb[1:1:2] | passed | 0.00013 seconds |
|
|
22
|
+
./spec/unit/tinker_payments_spec.rb[1:2:1] | passed | 0.0001 seconds |
|
|
23
|
+
./spec/unit/tinker_payments_spec.rb[1:2:2] | passed | 0.00009 seconds |
|
|
24
|
+
./spec/unit/tinker_payments_spec.rb[1:3:1] | passed | 0.00009 seconds |
|
|
25
|
+
./spec/unit/tinker_payments_spec.rb[1:3:2] | passed | 0.00007 seconds |
|
|
26
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:1:1] | passed | 0.00013 seconds |
|
|
27
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:1:2] | passed | 0.00004 seconds |
|
|
28
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:1:3] | passed | 0.00154 seconds |
|
|
29
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:1:4] | passed | 0.00021 seconds |
|
|
30
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:2:1] | passed | 0.00014 seconds |
|
|
31
|
+
./spec/unit/webhook/webhook_handler_spec.rb[1:2:2] | passed | 0.00008 seconds |
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.0
|
|
4
|
+
Exclude:
|
|
5
|
+
- 'vendor/**/*'
|
|
6
|
+
- 'spec/spec_helper.rb'
|
|
7
|
+
- 'bin/**/*'
|
|
8
|
+
- 'tmp/**/*'
|
|
9
|
+
- '*.gemspec'
|
|
10
|
+
|
|
11
|
+
Style/Documentation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Style/FrozenStringLiteralComment:
|
|
15
|
+
Enabled: true
|
|
16
|
+
|
|
17
|
+
Metrics/BlockLength:
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'spec/**/*'
|
|
20
|
+
- '*.gemspec'
|
|
21
|
+
|
|
22
|
+
Metrics/MethodLength:
|
|
23
|
+
Exclude:
|
|
24
|
+
- 'spec/**/*'
|
|
25
|
+
- 'lib/tinker/model/dto/**/*'
|
|
26
|
+
- 'lib/tinker/webhook/dto/**/*'
|
|
27
|
+
- 'lib/tinker/api/base_manager.rb'
|
|
28
|
+
- 'lib/tinker/auth/authentication_manager.rb'
|
|
29
|
+
- 'lib/tinker/model/transaction.rb'
|
|
30
|
+
Max: 15
|
|
31
|
+
|
|
32
|
+
Metrics/AbcSize:
|
|
33
|
+
Exclude:
|
|
34
|
+
- 'spec/**/*'
|
|
35
|
+
- 'lib/tinker/model/dto/**/*'
|
|
36
|
+
- 'lib/tinker/webhook/dto/**/*'
|
|
37
|
+
- 'lib/tinker/api/base_manager.rb'
|
|
38
|
+
- 'lib/tinker/auth/authentication_manager.rb'
|
|
39
|
+
- 'lib/tinker/http/http_client.rb'
|
|
40
|
+
Max: 20
|
|
41
|
+
|
|
42
|
+
Metrics/CyclomaticComplexity:
|
|
43
|
+
Exclude:
|
|
44
|
+
- 'spec/**/*'
|
|
45
|
+
Max: 10
|
|
46
|
+
|
|
47
|
+
Metrics/PerceivedComplexity:
|
|
48
|
+
Exclude:
|
|
49
|
+
- 'spec/**/*'
|
|
50
|
+
Max: 10
|
|
51
|
+
|
|
52
|
+
Metrics/ParameterLists:
|
|
53
|
+
Exclude:
|
|
54
|
+
- 'lib/tinker/model/dto/**/*'
|
|
55
|
+
|
|
56
|
+
Layout/LineLength:
|
|
57
|
+
Max: 120
|
|
58
|
+
Exclude:
|
|
59
|
+
- 'spec/**/*'
|
|
60
|
+
|
|
61
|
+
Naming/FileName:
|
|
62
|
+
Exclude:
|
|
63
|
+
- 'lib/tinker.rb'
|
|
64
|
+
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Tinker Payments Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for [Tinker Payments API](https://payments.tinker.co.ke/docs).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'tinker-pay-ruby-sdk'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install tinker-pay-ruby-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Ruby 3.0 or higher
|
|
28
|
+
- No external runtime dependencies (uses standard library)
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
require 'tinker'
|
|
34
|
+
|
|
35
|
+
tinker = Tinker::Payments.new(
|
|
36
|
+
api_public_key: 'your-public-key',
|
|
37
|
+
api_secret_key: 'your-secret-key'
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Initiate a Payment
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require 'tinker'
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
initiate_request = Tinker::Model::Dto::InitiatePaymentRequestDto.new(
|
|
50
|
+
amount: 100.00,
|
|
51
|
+
currency: 'KES',
|
|
52
|
+
gateway: Tinker::Enum::Gateway::MPESA,
|
|
53
|
+
merchant_reference: 'ORDER-12345',
|
|
54
|
+
return_url: 'https://your-app.com/payment/return',
|
|
55
|
+
customer_phone: '+254712345678',
|
|
56
|
+
transaction_desc: 'Payment for order #12345',
|
|
57
|
+
metadata: { order_id: '12345' }
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
transaction = tinker.transactions.initiate(initiate_request)
|
|
61
|
+
initiation_data = transaction.initiation_data
|
|
62
|
+
|
|
63
|
+
if initiation_data&.authorization_url
|
|
64
|
+
# Redirect user to authorization URL (Paystack, Stripe, etc.)
|
|
65
|
+
redirect_to initiation_data.authorization_url
|
|
66
|
+
end
|
|
67
|
+
rescue Tinker::Exception::ApiException => e
|
|
68
|
+
puts "API Error: #{e.message}"
|
|
69
|
+
rescue Tinker::Exception::NetworkException => e
|
|
70
|
+
puts "Network Error: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Note:** The `return_url` is where users are redirected after payment completion. Webhooks are configured separately in your dashboard.
|
|
75
|
+
|
|
76
|
+
### Query a Transaction
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
query_request = Tinker::Model::Dto::QueryPaymentRequestDto.new(
|
|
80
|
+
payment_reference: 'TXN-abc123xyz',
|
|
81
|
+
gateway: Tinker::Enum::Gateway::MPESA
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
transaction = tinker.transactions.query(query_request)
|
|
85
|
+
|
|
86
|
+
if transaction.successful?
|
|
87
|
+
query_data = transaction.query_data
|
|
88
|
+
puts "Amount: #{query_data.amount} #{query_data.currency}"
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Handle Webhooks
|
|
93
|
+
|
|
94
|
+
Webhooks support multiple event types: payment, subscription, invoice, and settlement. Check the event type and handle accordingly:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
require 'tinker'
|
|
98
|
+
|
|
99
|
+
event = tinker.webhooks.handle_from_request(request.body.read)
|
|
100
|
+
|
|
101
|
+
# Check event type
|
|
102
|
+
if event.payment_event?
|
|
103
|
+
payment_data = event.payment_data
|
|
104
|
+
# Handle payment.completed, payment.failed, etc.
|
|
105
|
+
elsif event.subscription_event?
|
|
106
|
+
subscription_data = event.subscription_data
|
|
107
|
+
# Handle subscription.created, subscription.cancelled, etc.
|
|
108
|
+
elsif event.invoice_event?
|
|
109
|
+
invoice_data = event.invoice_data
|
|
110
|
+
# Handle invoice.paid, invoice.failed
|
|
111
|
+
elsif event.settlement_event?
|
|
112
|
+
settlement_data = event.settlement_data
|
|
113
|
+
# Handle settlement.processed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Access event details
|
|
117
|
+
puts "Event type: #{event.type}" # e.g., "payment.completed"
|
|
118
|
+
puts "Event source: #{event.source}" # e.g., "payment"
|
|
119
|
+
puts "App ID: #{event.meta.app_id}"
|
|
120
|
+
puts "Signature: #{event.security.signature}"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For payment events only, you can convert to a `Transaction` object:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
transaction = tinker.webhooks.handle_as_transaction(request.body.read)
|
|
127
|
+
if transaction && transaction.successful?
|
|
128
|
+
callback_data = transaction.callback_data
|
|
129
|
+
puts "Payment successful: #{callback_data.reference}"
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Custom HTTP Client
|
|
134
|
+
|
|
135
|
+
You can use your own HTTP client by passing it to the constructor:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
require 'tinker'
|
|
139
|
+
|
|
140
|
+
# Create a custom HTTP client that responds to #post(url, headers:, body:)
|
|
141
|
+
# and returns an object with #status_code, #body, and #json methods
|
|
142
|
+
custom_client = MyCustomHttpClient.new
|
|
143
|
+
|
|
144
|
+
tinker = Tinker::Payments.new(
|
|
145
|
+
api_public_key: 'your-public-key',
|
|
146
|
+
api_secret_key: 'your-secret-key',
|
|
147
|
+
http_client: custom_client
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Documentation
|
|
152
|
+
|
|
153
|
+
For detailed API documentation, visit [Tinker Payments API Documentation](https://payments.tinker.co.ke/docs).
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
|
|
158
|
+
|
|
159
|
+
## Contributing
|
|
160
|
+
|
|
161
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/tinker/payments-ruby-sdk.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT License
|
|
166
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Tinker
|
|
6
|
+
module Api
|
|
7
|
+
class BaseManager
|
|
8
|
+
def initialize(config:, http_client:, auth_manager:)
|
|
9
|
+
@config = config
|
|
10
|
+
@http_client = http_client
|
|
11
|
+
@auth_manager = auth_manager
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def request(_method, endpoint, data = {})
|
|
17
|
+
base_url = @config.base_url.chomp('/')
|
|
18
|
+
endpoint = endpoint[1..] if endpoint.start_with?('/')
|
|
19
|
+
url = "#{base_url}/#{endpoint}"
|
|
20
|
+
|
|
21
|
+
token = @auth_manager.token
|
|
22
|
+
headers = {
|
|
23
|
+
'Authorization' => "Bearer #{token}",
|
|
24
|
+
'Accept' => 'application/json',
|
|
25
|
+
'Content-Type' => 'application/json'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body = data.empty? ? nil : JSON.generate(data)
|
|
29
|
+
|
|
30
|
+
response = @http_client.post(url, headers: headers, body: body)
|
|
31
|
+
result = response.json
|
|
32
|
+
|
|
33
|
+
if response.status_code >= 400
|
|
34
|
+
message = result.is_a?(Hash) ? (result['message'] || result['error'] || 'Unknown error') : 'Unknown error'
|
|
35
|
+
raise Exception::ApiException.new(message, Exception::ExceptionCode::API_ERROR)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result || {}
|
|
39
|
+
rescue Exception::ApiException => e
|
|
40
|
+
raise e
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
raise Exception::NetworkException.new("Failed to communicate with Tinker API: #{e.message}",
|
|
43
|
+
Exception::ExceptionCode::NETWORK_ERROR, e)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Api
|
|
5
|
+
class TransactionManager < BaseManager
|
|
6
|
+
def initiate(request)
|
|
7
|
+
payload = request.to_hash
|
|
8
|
+
response = request('POST', Config::Endpoints::PAYMENT_INITIATE_PATH, payload)
|
|
9
|
+
|
|
10
|
+
Model::Transaction.new(response)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def query(request)
|
|
14
|
+
payload = request.to_hash
|
|
15
|
+
response = request('POST', Config::Endpoints::PAYMENT_QUERY_PATH, payload)
|
|
16
|
+
|
|
17
|
+
Model::Transaction.new(response)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Tinker
|
|
8
|
+
module Auth
|
|
9
|
+
class AuthenticationManager
|
|
10
|
+
def initialize(config:, http_client:)
|
|
11
|
+
@config = config
|
|
12
|
+
@http_client = http_client
|
|
13
|
+
@token = nil
|
|
14
|
+
@expires_at = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def token
|
|
18
|
+
return @token if token_valid?
|
|
19
|
+
|
|
20
|
+
fetch_token
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def token_valid?
|
|
26
|
+
return false if @token.nil? || @expires_at.nil?
|
|
27
|
+
|
|
28
|
+
Time.now.to_i < (@expires_at - 60)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fetch_token
|
|
32
|
+
credentials = Base64.strict_encode64("#{@config.api_public_key}:#{@config.api_secret_key}")
|
|
33
|
+
|
|
34
|
+
url = Config::Endpoints::AUTH_TOKEN_URL
|
|
35
|
+
headers = {
|
|
36
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
37
|
+
'Accept' => 'application/json'
|
|
38
|
+
}
|
|
39
|
+
body = "credentials=#{URI.encode_www_form_component(credentials)}"
|
|
40
|
+
|
|
41
|
+
response = @http_client.post(url, headers: headers, body: body)
|
|
42
|
+
result = response.json
|
|
43
|
+
|
|
44
|
+
if response.status_code >= 400
|
|
45
|
+
message = result['message'] || 'Authentication failed'
|
|
46
|
+
raise Exception::AuthenticationException.new(message, Exception::ExceptionCode::AUTHENTICATION_ERROR)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless result['token']
|
|
50
|
+
raise Exception::NetworkException.new('Invalid authentication response: token missing', Exception::ExceptionCode::AUTHENTICATION_ERROR)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@token = result['token']
|
|
54
|
+
expires_in = result['expires_in'] || 3600
|
|
55
|
+
@expires_at = Time.now.to_i + expires_in
|
|
56
|
+
|
|
57
|
+
@token
|
|
58
|
+
rescue Exception::AuthenticationException, Exception::NetworkException => e
|
|
59
|
+
raise e
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
raise Exception::NetworkException.new("Failed to authenticate: #{e.message}",
|
|
62
|
+
Exception::ExceptionCode::AUTHENTICATION_ERROR, e)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Config
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :api_public_key, :api_secret_key, :base_url
|
|
7
|
+
|
|
8
|
+
def initialize(api_public_key:, api_secret_key:)
|
|
9
|
+
@api_public_key = api_public_key
|
|
10
|
+
@api_secret_key = api_secret_key
|
|
11
|
+
@base_url = "#{Endpoints::API_BASE_URL}/"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def api_key
|
|
15
|
+
@api_secret_key
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Config
|
|
5
|
+
class Endpoints
|
|
6
|
+
BASE_URL = 'https://payments.tinker.co.ke'
|
|
7
|
+
API_BASE_URL = "#{BASE_URL}/api".freeze
|
|
8
|
+
AUTH_TOKEN_URL = "#{BASE_URL}/auth/token".freeze
|
|
9
|
+
PAYMENT_INITIATE_PATH = '/payment/initiate'
|
|
10
|
+
PAYMENT_QUERY_PATH = '/payment/query'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class AuthenticationException < StandardError
|
|
6
|
+
def initialize(message, code = ExceptionCode::AUTHENTICATION_ERROR, previous = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@code = code
|
|
9
|
+
set_backtrace(previous.backtrace) if previous
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class ClientException < StandardError
|
|
6
|
+
def initialize(message, code = ExceptionCode::CLIENT_ERROR, previous = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@code = code
|
|
9
|
+
set_backtrace(previous.backtrace) if previous
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class ExceptionCode
|
|
6
|
+
API_ERROR = 1000
|
|
7
|
+
NETWORK_ERROR = 2000
|
|
8
|
+
AUTHENTICATION_ERROR = 3000
|
|
9
|
+
INVALID_PAYLOAD = 4000
|
|
10
|
+
WEBHOOK_ERROR = 5000
|
|
11
|
+
STREAM_ERROR = 6000
|
|
12
|
+
CLIENT_ERROR = 7000
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class InvalidPayloadException < StandardError
|
|
6
|
+
def initialize(message, code = ExceptionCode::INVALID_PAYLOAD, previous = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@code = code
|
|
9
|
+
set_backtrace(previous.backtrace) if previous
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class NetworkException < StandardError
|
|
6
|
+
def initialize(message, code = ExceptionCode::NETWORK_ERROR, previous = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@code = code
|
|
9
|
+
set_backtrace(previous.backtrace) if previous
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tinker
|
|
4
|
+
module Exception
|
|
5
|
+
class WebhookException < StandardError
|
|
6
|
+
def initialize(message, code = ExceptionCode::WEBHOOK_ERROR, previous = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@code = code
|
|
9
|
+
set_backtrace(previous.backtrace) if previous
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Tinker
|
|
8
|
+
module Http
|
|
9
|
+
class HttpClient
|
|
10
|
+
def initialize
|
|
11
|
+
@timeout = 30
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def post(url, headers: {}, body: nil)
|
|
15
|
+
uri = URI(url)
|
|
16
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
17
|
+
http.use_ssl = uri.scheme == 'https'
|
|
18
|
+
http.read_timeout = @timeout
|
|
19
|
+
http.open_timeout = @timeout
|
|
20
|
+
|
|
21
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
22
|
+
headers.each { |key, value| request[key] = value }
|
|
23
|
+
request.body = body if body
|
|
24
|
+
|
|
25
|
+
response = http.request(request)
|
|
26
|
+
Response.new(response.code.to_i, response.body, response.to_hash)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
raise Exception::NetworkException.new("Network error: #{e.message}", Exception::ExceptionCode::NETWORK_ERROR, e)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Response
|
|
32
|
+
attr_reader :status_code, :body, :headers
|
|
33
|
+
|
|
34
|
+
def initialize(status_code, body, headers)
|
|
35
|
+
@status_code = status_code
|
|
36
|
+
@body = body
|
|
37
|
+
@headers = headers
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def json
|
|
41
|
+
JSON.parse(@body)
|
|
42
|
+
rescue JSON::ParserError => e
|
|
43
|
+
raise Exception::InvalidPayloadException.new("Invalid JSON response: #{e.message}", Exception::ExceptionCode::INVALID_PAYLOAD)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|