paystack-gateway 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +25 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +206 -0
- data/Rakefile +12 -0
- data/lib/faraday/response/caching.rb +133 -0
- data/lib/faraday/response/mashify.rb +41 -0
- data/lib/paystack-gateway.rb +3 -0
- data/lib/paystack_gateway/api_error.rb +69 -0
- data/lib/paystack_gateway/configuration.rb +26 -0
- data/lib/paystack_gateway/current.rb +25 -0
- data/lib/paystack_gateway/customers.rb +44 -0
- data/lib/paystack_gateway/dedicated_virtual_accounts.rb +66 -0
- data/lib/paystack_gateway/miscellaneous.rb +27 -0
- data/lib/paystack_gateway/plans.rb +82 -0
- data/lib/paystack_gateway/refunds.rb +61 -0
- data/lib/paystack_gateway/request_module.rb +103 -0
- data/lib/paystack_gateway/response.rb +30 -0
- data/lib/paystack_gateway/subaccounts.rb +37 -0
- data/lib/paystack_gateway/transaction_response.rb +35 -0
- data/lib/paystack_gateway/transactions.rb +88 -0
- data/lib/paystack_gateway/transfer_recipients.rb +23 -0
- data/lib/paystack_gateway/transfers.rb +59 -0
- data/lib/paystack_gateway/verification.rb +33 -0
- data/lib/paystack_gateway/version.rb +5 -0
- data/lib/paystack_gateway/webhooks.rb +32 -0
- data/lib/paystack_gateway.rb +25 -0
- data/sig/paystack_gateway.rbs +4 -0
- metadata +122 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
|
5
|
+
# = PaystackGateway
|
6
|
+
module PaystackGateway
|
7
|
+
# Encapsulates the configuration options for PaystackGateway including the
|
8
|
+
# secret key, logger, logging_options, and log filter.
|
9
|
+
class Configuration
|
10
|
+
attr_accessor :secret_key, :logger, :logging_options, :log_filter
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@logger = Logger.new($stdout)
|
14
|
+
@log_filter = lambda(&:dup)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_writer :config
|
20
|
+
|
21
|
+
delegate :secret_key, :logger, :logging_options, :log_filter, to: :config
|
22
|
+
|
23
|
+
def config = @config ||= Configuration.new
|
24
|
+
def configure = yield(config)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/isolated_execution_state'
|
4
|
+
require 'active_support/code_generator'
|
5
|
+
require 'active_support/current_attributes'
|
6
|
+
|
7
|
+
module PaystackGateway
|
8
|
+
# Global singleton providing thread isolated attributes, used and reset around
|
9
|
+
# each api method call.
|
10
|
+
class Current < ActiveSupport::CurrentAttributes
|
11
|
+
attribute :api_module, :api_method_name
|
12
|
+
|
13
|
+
def response_class
|
14
|
+
class_name = "#{api_method_name}_response".camelize.to_sym
|
15
|
+
api_module.const_defined?(class_name) ? api_module.const_get(class_name) : PaystackGateway::Response
|
16
|
+
end
|
17
|
+
|
18
|
+
def error_class
|
19
|
+
class_name = "#{api_method_name}_error".camelize
|
20
|
+
api_module.const_defined?(class_name) ? api_module.const_get(class_name) : PaystackGateway::ApiError
|
21
|
+
end
|
22
|
+
|
23
|
+
def qualified_api_method_name = "#{api_module.name}##{api_method_name}"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage customers https://paystack.com/docs/api/customer/
|
5
|
+
module Customers
|
6
|
+
include PaystackGateway::RequestModule
|
7
|
+
|
8
|
+
# Common helpers for response from customer endpoints
|
9
|
+
module CustomerResponse
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
delegate :id, :customer_code, to: :data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class CreateCustomerResponse < PaystackGateway::Response
|
18
|
+
include CustomerResponse
|
19
|
+
end
|
20
|
+
|
21
|
+
api_method def self.create_customer(email:, first_name:, last_name:)
|
22
|
+
use_connection do |connection|
|
23
|
+
connection.post('/customer', { email:, first_name:, last_name: })
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Response from GET /customer/:email_or_id
|
28
|
+
class FetchCustomerResponse < PaystackGateway::Response
|
29
|
+
include CustomerResponse
|
30
|
+
|
31
|
+
delegate :subscriptions, :authorizations, to: :data
|
32
|
+
|
33
|
+
def active_subscriptions = subscriptions.select { _1.status == 'active' }
|
34
|
+
def active_subscription_codes = active_subscriptions.map(&:subscription_code)
|
35
|
+
def reusable_authorizations = authorizations.select(&:reusable)
|
36
|
+
end
|
37
|
+
|
38
|
+
api_method def self.fetch_customer(email:)
|
39
|
+
use_connection do |connection|
|
40
|
+
connection.get("/customer/#{email}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# The Dedicated Virtual Account API enables Nigerian merchants to manage unique
|
5
|
+
# payment accounts of their customers.
|
6
|
+
# https://paystack.com/docs/api/dedicated-virtual-account/
|
7
|
+
module DedicatedVirtualAccounts
|
8
|
+
include PaystackGateway::RequestModule
|
9
|
+
|
10
|
+
# Response from POST /dedicated_account endpoint.
|
11
|
+
class CreateDedicatedVirtualAccountResponse < PaystackGateway::Response; end
|
12
|
+
|
13
|
+
api_method def self.create_dedicated_virtual_account(customer_id_or_code:, subaccount_code:, preferred_bank:)
|
14
|
+
use_connection do |connection|
|
15
|
+
connection.post(
|
16
|
+
'/dedicated_account',
|
17
|
+
{
|
18
|
+
customer: customer_id_or_code,
|
19
|
+
preferred_bank:,
|
20
|
+
subaccount: subaccount_code,
|
21
|
+
phone: '+2348011111111', # phone number is required by paystack for some reason, use fake phone number
|
22
|
+
},
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Response from POST /dedicated_account/assign endpoint.
|
28
|
+
class AssignDedicatedVirtualAccountResponse < PaystackGateway::Response; end
|
29
|
+
|
30
|
+
api_method def self.assign_dedicated_virtual_account(
|
31
|
+
email:, first_name:, last_name:, subaccount_code:, preferred_bank:
|
32
|
+
)
|
33
|
+
use_connection do |connection|
|
34
|
+
connection.post(
|
35
|
+
'/dedicated_account/assign',
|
36
|
+
{
|
37
|
+
email:,
|
38
|
+
first_name:,
|
39
|
+
last_name:,
|
40
|
+
preferred_bank:,
|
41
|
+
country: :NG,
|
42
|
+
subaccount: subaccount_code,
|
43
|
+
},
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Response from POST /dedicated_account/split endpoint
|
49
|
+
class SplitDedicatedAccountTransactionResponse < PaystackGateway::Response; end
|
50
|
+
|
51
|
+
api_method def self.split_dedicated_account_transaction(customer_id_or_code:, subaccount_code:)
|
52
|
+
use_connection do |connection|
|
53
|
+
connection.post('/dedicated_account/split', { customer: customer_id_or_code, subaccount: subaccount_code })
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Response from GET /dedicated_account endpoint
|
58
|
+
class RequeryDedicatedAccountResponse < PaystackGateway::Response; end
|
59
|
+
|
60
|
+
api_method def self.requery_dedicated_account(account_number:, bank:)
|
61
|
+
use_connection do |connection|
|
62
|
+
connection.get('/dedicated_account', { account_number:, provider_slug: bank })
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Supporting APIs that can be used to provide more details to other APIs
|
5
|
+
# https://paystack.com/docs/api/#miscellaneous
|
6
|
+
module Miscellaneous
|
7
|
+
include PaystackGateway::RequestModule
|
8
|
+
|
9
|
+
# Response from GET /bank endpoint.
|
10
|
+
class ListBanksResponse < PaystackGateway::Response
|
11
|
+
def bank_names = data.map(&:name)
|
12
|
+
def bank_slugs = data.map(&:slug)
|
13
|
+
|
14
|
+
def bank_details(*attributes) = data.map { _1.slice(*attributes) }
|
15
|
+
|
16
|
+
def by_bank_names = data.index_by(&:name)
|
17
|
+
def by_bank_codes = data.index_by(&:code)
|
18
|
+
end
|
19
|
+
|
20
|
+
# https://paystack.com/docs/api/miscellaneous/#bank
|
21
|
+
api_method def self.list_banks(use_cache: true, pay_with_bank_transfer: false)
|
22
|
+
use_connection(cache_options: use_cache ? {} : nil) do |connection|
|
23
|
+
connection.get('/bank', pay_with_bank_transfer ? { pay_with_bank_transfer: } : {})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage installment payment options
|
5
|
+
# https://paystack.com/docs/api/plan/#create
|
6
|
+
module Plans
|
7
|
+
include PaystackGateway::RequestModule
|
8
|
+
|
9
|
+
# Response from POST /plan endpoint.
|
10
|
+
class CreatePlanResponse < PaystackGateway::Response
|
11
|
+
delegate :id, to: :data, prefix: :plan
|
12
|
+
delegate :id, :plan_code, to: :data
|
13
|
+
end
|
14
|
+
|
15
|
+
api_method def self.create_plan(name:, amount:, interval:)
|
16
|
+
use_connection do |connection|
|
17
|
+
connection.post(
|
18
|
+
'/plan',
|
19
|
+
{
|
20
|
+
name:,
|
21
|
+
interval:,
|
22
|
+
amount: amount * 100,
|
23
|
+
send_invoices: false,
|
24
|
+
send_sms: false,
|
25
|
+
},
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Response from GET /plan endpoint.
|
31
|
+
class ListPlansResponse < PaystackGateway::Response
|
32
|
+
def active_plans = data.select { |plan| !plan.is_deleted && !plan.is_archived }
|
33
|
+
|
34
|
+
def find_active_plan_by_name(name)
|
35
|
+
active_plans.sort_by { -Time.parse(_1.createdAt).to_i }.find { _1.name == name }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
api_method def self.list_plans
|
40
|
+
use_connection do |connection|
|
41
|
+
connection.get('/plan')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Response from GET /plan/:code endpoint.
|
46
|
+
class FetchPlanResponse < PaystackGateway::Response
|
47
|
+
delegate :subscriptions, to: :data
|
48
|
+
|
49
|
+
def active_subscriptions = subscriptions.select { _1.status.to_sym == :active }
|
50
|
+
|
51
|
+
def active_subscription_codes(email: nil)
|
52
|
+
subscriptions =
|
53
|
+
if email
|
54
|
+
active_subscriptions.select { _1.customer.email.casecmp?(email) }
|
55
|
+
else
|
56
|
+
active_subscriptions
|
57
|
+
end
|
58
|
+
subscriptions.map(&:subscription_code)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
api_method def self.fetch_plan(code:)
|
63
|
+
use_connection do |connection|
|
64
|
+
connection.get("/plan/#{code}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class UpdatePlanResponse < PaystackGateway::Response; end
|
69
|
+
|
70
|
+
api_method def self.update_plan(code:, amount:, interval:)
|
71
|
+
use_connection do |connection|
|
72
|
+
connection.put(
|
73
|
+
"/plan/#{code}",
|
74
|
+
{
|
75
|
+
amount: amount * 100,
|
76
|
+
interval:,
|
77
|
+
},
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage transaction refunds.
|
5
|
+
# https://paystack.com/docs/api/refund
|
6
|
+
module Refunds
|
7
|
+
include PaystackGateway::RequestModule
|
8
|
+
|
9
|
+
# Common helpers for responses from refunds endpoints
|
10
|
+
module TransactionRefundResponse
|
11
|
+
def refund_success? = transaction_status == :processed
|
12
|
+
def refund_failed? = transaction_status == :failed
|
13
|
+
def refund_pending? = transaction_status.in?(%i[pending processing])
|
14
|
+
end
|
15
|
+
|
16
|
+
# Response from POST /refund endpoint.
|
17
|
+
class CreateResponse < PaystackGateway::Response
|
18
|
+
include TransactionResponse
|
19
|
+
include TransactionRefundResponse
|
20
|
+
end
|
21
|
+
|
22
|
+
api_method def self.create(transaction_reference_or_id:)
|
23
|
+
use_connection do |connection|
|
24
|
+
connection.post('/refund', { transaction: transaction_reference_or_id })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Response from GET /refund endpoint.
|
29
|
+
class ListRefundsResponse < PaystackGateway::Response
|
30
|
+
def pending_or_successful
|
31
|
+
filtered = data.select { _1.status&.to_sym.in?(%i[processed pending processing]) }
|
32
|
+
|
33
|
+
ListRefundsResponse.new({ **self, data: filtered })
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_amount(amount)
|
37
|
+
filtered = data.select { _1.amount == amount * 100 }
|
38
|
+
|
39
|
+
ListRefundsResponse.new({ **self, data: filtered })
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
api_method def self.list_refunds(transaction_id:)
|
44
|
+
use_connection do |connection|
|
45
|
+
connection.get('/refund', { transaction: transaction_id })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Response from GET /refund/:id endpoint.
|
50
|
+
class FetchRefundResponse < PaystackGateway::Response
|
51
|
+
include TransactionResponse
|
52
|
+
include TransactionRefundResponse
|
53
|
+
end
|
54
|
+
|
55
|
+
api_method def self.fetch_refund(refund_id:)
|
56
|
+
use_connection do |connection|
|
57
|
+
connection.get("/refund/#{refund_id}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday/response/caching'
|
5
|
+
require 'faraday/response/mashify'
|
6
|
+
require 'active_support/cache'
|
7
|
+
require 'active_support/notifications'
|
8
|
+
require 'active_support/concern'
|
9
|
+
require 'active_support/core_ext/string/inflections' # for #camelize
|
10
|
+
require 'active_support/core_ext/numeric/time' # for #days
|
11
|
+
|
12
|
+
module PaystackGateway
|
13
|
+
# This module provides methods for making API requests to the Paystack
|
14
|
+
# payment gateway, including handling responses and errors.
|
15
|
+
module RequestModule
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
BASE_URL = 'https://api.paystack.co'
|
19
|
+
|
20
|
+
# ClassMethods
|
21
|
+
module ClassMethods
|
22
|
+
def api_methods = (@api_method_names || Set.new).to_a
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def use_connection(response_class = Current.response_class, cache_options: nil)
|
27
|
+
connection = Faraday.new(BASE_URL) do |conn|
|
28
|
+
conn.request :json
|
29
|
+
conn.request :authorization, 'Bearer', PaystackGateway.secret_key
|
30
|
+
|
31
|
+
conn.response :logger, PaystackGateway.logger, { headers: false, **(PaystackGateway.logging_options || {}) }
|
32
|
+
conn.response :mashify, mash_class: response_class
|
33
|
+
|
34
|
+
conn.response :raise_error
|
35
|
+
conn.response :json
|
36
|
+
conn.response :caching, cache_store(**cache_options) if cache_options
|
37
|
+
end
|
38
|
+
|
39
|
+
if block_given?
|
40
|
+
response = yield connection
|
41
|
+
response.body
|
42
|
+
else
|
43
|
+
connection
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def cache_store(expires_in: 7.days.to_i, cache_key: nil)
|
48
|
+
cache_dir = File.join(ENV['TMPDIR'] || '/tmp', 'cache')
|
49
|
+
namespace = cache_key ? "#{name}_#{cache_key}" : name
|
50
|
+
|
51
|
+
ActiveSupport::Cache::FileStore.new cache_dir, namespace:, expires_in: expires_in.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def api_method(method_name)
|
55
|
+
@api_method_names ||= Set.new
|
56
|
+
@api_method_names << method_name
|
57
|
+
|
58
|
+
decorate_api_methods(method_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def decorate_api_methods(*method_names)
|
62
|
+
singleton_class.class_exec do
|
63
|
+
prepend(Module.new do
|
64
|
+
method_names.flatten.each do |method_name|
|
65
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
66
|
+
Current.with(api_module: self, api_method_name: method_name) do
|
67
|
+
super(*args, **kwargs, &block)
|
68
|
+
rescue Faraday::Error => e
|
69
|
+
handle_error(e)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_error(error)
|
78
|
+
PaystackGateway.logger.error "#{Current.qualified_api_method_name}: #{error.message}"
|
79
|
+
PaystackGateway.logger.error JSON.pretty_generate(filtered_response(error.response) || {}) if error.response
|
80
|
+
|
81
|
+
raise Current.error_class.new(
|
82
|
+
"Paystack error: #{error.message}, status: #{error.response_status}, response: #{error.response_body}",
|
83
|
+
original_error: error,
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def filtered_response(response)
|
88
|
+
return unless response
|
89
|
+
|
90
|
+
{
|
91
|
+
request_method: response.dig(:request, :method),
|
92
|
+
request_url: response.dig(:request, :url),
|
93
|
+
request_headers: PaystackGateway.log_filter.call(response.dig(:request, :headers)),
|
94
|
+
request_body: PaystackGateway.log_filter.call(JSON.parse(response.dig(:request, :body) || '{}')),
|
95
|
+
|
96
|
+
response_status: response[:status],
|
97
|
+
response_headers: PaystackGateway.log_filter.call(response[:headers]),
|
98
|
+
response_body: PaystackGateway.log_filter.call(response[:body]),
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hashie/dash'
|
4
|
+
require 'hashie/mash'
|
5
|
+
require 'hashie/extensions/coercion'
|
6
|
+
|
7
|
+
module PaystackGateway
|
8
|
+
# Wrapper for responses from Paystack.
|
9
|
+
class Response < Hashie::Dash
|
10
|
+
include Hashie::Extensions::Coercion
|
11
|
+
|
12
|
+
property :status
|
13
|
+
property :message
|
14
|
+
property :data
|
15
|
+
property :meta
|
16
|
+
|
17
|
+
coerce_key :data, ->(v) { coerce_data(v) }
|
18
|
+
|
19
|
+
def self.coerce_data(value)
|
20
|
+
case value
|
21
|
+
when Array
|
22
|
+
value.map { coerce_data(_1) }
|
23
|
+
when Hash
|
24
|
+
Hashie::Mash.new(value)
|
25
|
+
else
|
26
|
+
value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage subaccounts https://paystack.com/docs/api/subaccount/
|
5
|
+
module Subaccounts
|
6
|
+
include PaystackGateway::RequestModule
|
7
|
+
|
8
|
+
# Response from POST /subaccount
|
9
|
+
class CreateSubaccountResponse < PaystackGateway::Response
|
10
|
+
delegate :subaccount_code, to: :data
|
11
|
+
end
|
12
|
+
|
13
|
+
api_method def self.create_subaccount(business_name:, settlement_bank:, account_number:, percentage_charge:)
|
14
|
+
use_connection do |connection|
|
15
|
+
connection.post(
|
16
|
+
'/subaccount',
|
17
|
+
{ business_name:, settlement_bank:, account_number:, percentage_charge: }.compact,
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Response from PUT /subaccount/:id_or_code
|
23
|
+
class UpdateSubaccountResponse < CreateSubaccountResponse; end
|
24
|
+
|
25
|
+
api_method def self.update_subaccount(
|
26
|
+
subaccount_code,
|
27
|
+
business_name: nil, settlement_bank: nil, account_number: nil, percentage_charge: nil
|
28
|
+
)
|
29
|
+
use_connection do |connection|
|
30
|
+
connection.put(
|
31
|
+
"/subaccount/#{subaccount_code}",
|
32
|
+
{ business_name:, settlement_bank:, account_number:, percentage_charge: }.compact,
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Common helpers for responses from transaction endpoints
|
5
|
+
module TransactionResponse
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :id, :amount, :subaccount, :fees_split, to: :data
|
10
|
+
|
11
|
+
attr_writer :completed_at
|
12
|
+
end
|
13
|
+
|
14
|
+
def transaction_success? = transaction_status.in?(%i[success reversed reversal_pending])
|
15
|
+
def transaction_abandoned? = transaction_status == :abandoned
|
16
|
+
def transaction_failed? = transaction_status == :failed
|
17
|
+
def transaction_pending? = transaction_status.in?(%i[pending ongoing])
|
18
|
+
|
19
|
+
def transaction_status = data.status.to_sym
|
20
|
+
def transaction_amount = amount / BigDecimal('100')
|
21
|
+
def transaction_completed_at = data[:updatedAt] || @completed_at
|
22
|
+
|
23
|
+
def subaccount_amount
|
24
|
+
return if !subaccount || !fees_split
|
25
|
+
|
26
|
+
fees_split.subaccount / BigDecimal('100')
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_reason
|
30
|
+
return if !transaction_failed? && !transaction_abandoned?
|
31
|
+
|
32
|
+
data.gateway_response || transaction_status || message
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage payments https://paystack.com/docs/api/#transaction
|
5
|
+
module Transactions
|
6
|
+
include PaystackGateway::RequestModule
|
7
|
+
|
8
|
+
# Response from POST /transaction/initialize endpoint.
|
9
|
+
class InitializeTransactionResponse < PaystackGateway::Response
|
10
|
+
delegate :authorization_url, to: :data
|
11
|
+
|
12
|
+
alias payment_url authorization_url
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised when an error occurs while calling POST /transaction/initialize
|
16
|
+
class InitializeTransactionError < ApiError
|
17
|
+
def cancellable? = super || network_error?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Response looks like this.
|
21
|
+
# {
|
22
|
+
# "status": true,
|
23
|
+
# "message": "Authorization URL created",
|
24
|
+
# "data": {
|
25
|
+
# "authorization_url": "https://checkout.paystack.com/n4ysyedbuseog8c",
|
26
|
+
# "access_code": "n4ysyedbuseog8c",
|
27
|
+
# "reference": "hlr4bxhypt"
|
28
|
+
# }
|
29
|
+
# }
|
30
|
+
api_method def self.initialize_transaction(**transaction_data)
|
31
|
+
raise ApiError.new(:invalid_amount, cancellable: true) if transaction_data[:amount].blank?
|
32
|
+
raise ApiError.new(:invalid_reference, cancellable: true) if transaction_data[:reference].blank?
|
33
|
+
raise ApiError.new(:invalid_email, cancellable: true) if transaction_data[:email].blank?
|
34
|
+
|
35
|
+
transaction_data[:amount] = (transaction_data[:amount] * 100).to_i
|
36
|
+
|
37
|
+
use_connection do |connection|
|
38
|
+
connection.post('/transaction/initialize', transaction_data.compact)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Response from GET /transaction/verify/:reference endpoint.
|
43
|
+
class VerifyTransactionResponse < PaystackGateway::Response
|
44
|
+
include TransactionResponse
|
45
|
+
|
46
|
+
delegate :paid_at, to: :data
|
47
|
+
|
48
|
+
def transaction_completed_at
|
49
|
+
paid_at || super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Raised when an error occurs while calling /transactions/verify/:reference
|
54
|
+
class VerifyTransactionError < ApiError
|
55
|
+
def transaction_not_found?
|
56
|
+
return false if !response_body
|
57
|
+
|
58
|
+
response_body[:status] == false && response_body[:message].match?(/transaction reference not found/i)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
api_method def self.verify_transaction(reference:)
|
63
|
+
raise VerifyTransactionError, :invalid_reference if reference.blank?
|
64
|
+
|
65
|
+
use_connection do |connection|
|
66
|
+
connection.get("/transaction/verify/#{reference}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Response from POST /transaction/charge_authorization endpoint.
|
71
|
+
class ChargeAuthorizationResponse < PaystackGateway::Response
|
72
|
+
include TransactionResponse
|
73
|
+
end
|
74
|
+
|
75
|
+
api_method def self.charge_authorization(**transaction_data)
|
76
|
+
raise ApiError.new(:invalid_amount, cancellable: true) if transaction_data[:amount].blank?
|
77
|
+
raise ApiError.new(:invalid_reference, cancellable: true) if transaction_data[:reference].blank?
|
78
|
+
raise ApiError.new(:invalid_authorization_code, cancellable: true) if transaction_data[:authorization_code].blank?
|
79
|
+
raise ApiError.new(:invalid_email, cancellable: true) if transaction_data[:email].blank?
|
80
|
+
|
81
|
+
transaction_data[:amount] = (transaction_data[:amount] * 100).to_i
|
82
|
+
|
83
|
+
use_connection do |connection|
|
84
|
+
connection.post('/transaction/charge_authorization', transaction_data.compact)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PaystackGateway
|
4
|
+
# Create and manage beneficiaries that you send money to
|
5
|
+
# https://paystack.com/docs/api/#transfer-recipient
|
6
|
+
module TransferRecipients
|
7
|
+
include PaystackGateway::RequestModule
|
8
|
+
|
9
|
+
# Response from POST /transferrecipient endpoint.
|
10
|
+
class CreateTransferRecipientResponse < PaystackGateway::Response
|
11
|
+
delegate :id, :recipient_code, to: :data
|
12
|
+
end
|
13
|
+
|
14
|
+
api_method def self.create_transfer_recipient(name:, account_number:, bank_code:)
|
15
|
+
use_connection do |connection|
|
16
|
+
connection.post(
|
17
|
+
'/transferrecipient',
|
18
|
+
{ type: :nuban, name: name, account_number: account_number, bank_code: bank_code, currency: :NGN },
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|