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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec_status +31 -0
  3. data/.rubocop.yml +64 -0
  4. data/Gemfile +11 -0
  5. data/README.md +166 -0
  6. data/Rakefile +8 -0
  7. data/lib/tinker/api/base_manager.rb +47 -0
  8. data/lib/tinker/api/transaction_manager.rb +21 -0
  9. data/lib/tinker/auth/authentication_manager.rb +66 -0
  10. data/lib/tinker/config/configuration.rb +19 -0
  11. data/lib/tinker/config/endpoints.rb +13 -0
  12. data/lib/tinker/enum/gateway.rb +11 -0
  13. data/lib/tinker/enum/payment_status.rb +12 -0
  14. data/lib/tinker/exception/api_exception.rb +14 -0
  15. data/lib/tinker/exception/authentication_exception.rb +15 -0
  16. data/lib/tinker/exception/client_exception.rb +15 -0
  17. data/lib/tinker/exception/exception_code.rb +15 -0
  18. data/lib/tinker/exception/invalid_payload_exception.rb +15 -0
  19. data/lib/tinker/exception/network_exception.rb +15 -0
  20. data/lib/tinker/exception/webhook_exception.rb +15 -0
  21. data/lib/tinker/http/http_client.rb +48 -0
  22. data/lib/tinker/model/dto/callback_data_dto.rb +40 -0
  23. data/lib/tinker/model/dto/initiate_payment_request_dto.rb +44 -0
  24. data/lib/tinker/model/dto/initiation_data_dto.rb +30 -0
  25. data/lib/tinker/model/dto/query_data_dto.rb +40 -0
  26. data/lib/tinker/model/dto/query_payment_request_dto.rb +25 -0
  27. data/lib/tinker/model/transaction.rb +49 -0
  28. data/lib/tinker/payments.rb +34 -0
  29. data/lib/tinker/version.rb +5 -0
  30. data/lib/tinker/webhook/dto/invoice_event_data_dto.rb +35 -0
  31. data/lib/tinker/webhook/dto/payment_event_data_dto.rb +40 -0
  32. data/lib/tinker/webhook/dto/settlement_event_data_dto.rb +33 -0
  33. data/lib/tinker/webhook/dto/subscription_event_data_dto.rb +35 -0
  34. data/lib/tinker/webhook/webhook_event.rb +75 -0
  35. data/lib/tinker/webhook/webhook_handler.rb +35 -0
  36. data/lib/tinker/webhook/webhook_meta.rb +15 -0
  37. data/lib/tinker/webhook/webhook_security.rb +14 -0
  38. data/lib/tinker.rb +33 -0
  39. 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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.12'
9
+ gem 'rubocop', '~> 1.57', require: false
10
+ gem 'webmock', '~> 3.18'
11
+ end
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinker
4
+ module Enum
5
+ class Gateway
6
+ MPESA = 'mpesa'
7
+ PAYSTACK = 'paystack'
8
+ STRIPE = 'stripe'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinker
4
+ module Enum
5
+ class PaymentStatus
6
+ PENDING = 'pending'
7
+ SUCCESS = 'success'
8
+ CANCELLED = 'cancelled'
9
+ FAILED = 'failed'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinker
4
+ module Exception
5
+ class ApiException < StandardError
6
+ def initialize(message, code = ExceptionCode::API_ERROR)
7
+ super(message)
8
+ @code = code
9
+ end
10
+
11
+ attr_reader :code
12
+ end
13
+ end
14
+ 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