paystack-gateway 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/.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
|