dk_payment_gateway 1.0.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 +4 -0
- data/.rspec_status +11 -0
- data/API_REFERENCE.md +458 -0
- data/CHANGELOG.md +64 -0
- data/DEVELOPMENT.md +380 -0
- data/EXAMPLES.md +491 -0
- data/FILE_STRUCTURE.md +407 -0
- data/Gemfile +13 -0
- data/INSTALLATION.md +460 -0
- data/LICENSE +22 -0
- data/PROJECT_OVERVIEW.md +314 -0
- data/QUICK_START.md +186 -0
- data/README.md +296 -0
- data/Rakefile +9 -0
- data/SUMMARY.md +285 -0
- data/dk_payment_gateway.gemspec +40 -0
- data/examples/README.md +199 -0
- data/examples/generate_qr.rb +110 -0
- data/examples/intra_transfer.rb +114 -0
- data/examples/simple_payment.rb +102 -0
- data/lib/dk_payment_gateway/authentication.rb +102 -0
- data/lib/dk_payment_gateway/client.rb +139 -0
- data/lib/dk_payment_gateway/configuration.rb +32 -0
- data/lib/dk_payment_gateway/errors.rb +39 -0
- data/lib/dk_payment_gateway/intra_transaction.rb +147 -0
- data/lib/dk_payment_gateway/pull_payment.rb +155 -0
- data/lib/dk_payment_gateway/qr_payment.rb +98 -0
- data/lib/dk_payment_gateway/signature.rb +72 -0
- data/lib/dk_payment_gateway/transaction_status.rb +127 -0
- data/lib/dk_payment_gateway/utils.rb +161 -0
- data/lib/dk_payment_gateway/version.rb +5 -0
- data/lib/dk_payment_gateway.rb +28 -0
- metadata +160 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DkPaymentGateway
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :config, :access_token, :private_key
|
|
9
|
+
|
|
10
|
+
def initialize(config = nil)
|
|
11
|
+
@config = config || DkPaymentGateway.configuration
|
|
12
|
+
validate_configuration!
|
|
13
|
+
@access_token = nil
|
|
14
|
+
@private_key = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Authentication methods
|
|
18
|
+
def authenticate!
|
|
19
|
+
auth = Authentication.new(self)
|
|
20
|
+
@access_token = auth.fetch_token
|
|
21
|
+
@private_key = auth.fetch_private_key
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Pull Payment methods
|
|
26
|
+
def pull_payment
|
|
27
|
+
@pull_payment ||= PullPayment.new(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Intra Transaction methods
|
|
31
|
+
def intra_transaction
|
|
32
|
+
@intra_transaction ||= IntraTransaction.new(self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# QR Payment methods
|
|
36
|
+
def qr_payment
|
|
37
|
+
@qr_payment ||= QrPayment.new(self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Transaction Status methods
|
|
41
|
+
def transaction_status
|
|
42
|
+
@transaction_status ||= TransactionStatus.new(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# HTTP request methods
|
|
46
|
+
def post(path, body: {}, headers: {}, skip_auth: false)
|
|
47
|
+
request(:post, path, body: body, headers: headers, skip_auth: skip_auth)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(path, params: {}, headers: {}, skip_auth: false)
|
|
51
|
+
request(:get, path, params: params, headers: headers, skip_auth: skip_auth)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def validate_configuration!
|
|
57
|
+
raise ConfigurationError, "Configuration is required" if config.nil?
|
|
58
|
+
unless config.valid?
|
|
59
|
+
raise ConfigurationError, "Missing required configuration fields: #{config.missing_fields.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def request(method, path, body: {}, params: {}, headers: {}, skip_auth: false)
|
|
64
|
+
url = "#{config.base_url}#{path}"
|
|
65
|
+
|
|
66
|
+
response = connection.send(method) do |req|
|
|
67
|
+
req.url path
|
|
68
|
+
req.headers = build_headers(headers, skip_auth)
|
|
69
|
+
req.body = body.to_json if method == :post && !body.empty?
|
|
70
|
+
req.params = params if method == :get && !params.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
handle_response(response)
|
|
74
|
+
rescue Faraday::Error => e
|
|
75
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def connection
|
|
79
|
+
@connection ||= Faraday.new(url: config.base_url) do |conn|
|
|
80
|
+
conn.request :json
|
|
81
|
+
conn.response :json, content_type: /\bjson$/
|
|
82
|
+
conn.adapter Faraday.default_adapter
|
|
83
|
+
conn.options.timeout = config.timeout
|
|
84
|
+
conn.options.open_timeout = config.open_timeout
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_headers(custom_headers = {}, skip_auth = false)
|
|
89
|
+
headers = {
|
|
90
|
+
"Content-Type" => "application/json",
|
|
91
|
+
"X-gravitee-api-key" => config.api_key
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
unless skip_auth
|
|
95
|
+
headers["Authorization"] = "Bearer #{access_token}" if access_token
|
|
96
|
+
headers["source_app"] = config.source_app
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
headers.merge(custom_headers)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_response(response)
|
|
103
|
+
case response.status
|
|
104
|
+
when 200..299
|
|
105
|
+
response.body
|
|
106
|
+
when 400..499
|
|
107
|
+
handle_client_error(response)
|
|
108
|
+
when 500..599
|
|
109
|
+
handle_server_error(response)
|
|
110
|
+
else
|
|
111
|
+
raise APIError, "Unexpected response status: #{response.status}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_client_error(response)
|
|
116
|
+
body = response.body || {}
|
|
117
|
+
error_message = body["response_message"] || body["response_detail"] || "Client error"
|
|
118
|
+
|
|
119
|
+
raise InvalidParameterError.new(
|
|
120
|
+
error_message,
|
|
121
|
+
response_code: body["response_code"],
|
|
122
|
+
response_detail: body["response_detail"]
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_server_error(response)
|
|
127
|
+
body = response.body || {}
|
|
128
|
+
error_message = body["response_description"] || body["response_message"] || "Server error"
|
|
129
|
+
|
|
130
|
+
raise APIError.new(
|
|
131
|
+
error_message,
|
|
132
|
+
response_code: body["response_code"],
|
|
133
|
+
response_message: body["response_message"],
|
|
134
|
+
response_description: body["response_description"]
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DkPaymentGateway
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :base_url, :api_key, :username, :password, :client_id,
|
|
6
|
+
:client_secret, :source_app, :timeout, :open_timeout
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@base_url = ENV['DK_BASE_URL']
|
|
10
|
+
@api_key = ENV['DK_API_KEY']
|
|
11
|
+
@username = ENV['DK_USERNAME']
|
|
12
|
+
@password = ENV['DK_PASSWORD']
|
|
13
|
+
@client_id = ENV['DK_CLIENT_ID']
|
|
14
|
+
@client_secret = ENV['DK_CLIENT_SECRET']
|
|
15
|
+
@source_app = ENV['DK_SOURCE_APP']
|
|
16
|
+
@timeout = 30
|
|
17
|
+
@open_timeout = 10
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
required_fields.all? { |field| !send(field).nil? && !send(field).to_s.empty? }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def required_fields
|
|
25
|
+
%i[base_url api_key username password client_id client_secret source_app]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def missing_fields
|
|
29
|
+
required_fields.reject { |field| !send(field).nil? && !send(field).to_s.empty? }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DkPaymentGateway
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class AuthenticationError < Error; end
|
|
9
|
+
|
|
10
|
+
class InvalidParameterError < Error
|
|
11
|
+
attr_reader :response_code, :response_detail
|
|
12
|
+
|
|
13
|
+
def initialize(message, response_code: nil, response_detail: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@response_code = response_code
|
|
16
|
+
@response_detail = response_detail
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class APIError < Error
|
|
21
|
+
attr_reader :response_code, :response_message, :response_description, :response_detail
|
|
22
|
+
|
|
23
|
+
def initialize(message, response_code: nil, response_message: nil,
|
|
24
|
+
response_description: nil, response_detail: nil)
|
|
25
|
+
super(message)
|
|
26
|
+
@response_code = response_code
|
|
27
|
+
@response_message = response_message
|
|
28
|
+
@response_description = response_description
|
|
29
|
+
@response_detail = response_detail
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class NetworkError < Error; end
|
|
34
|
+
|
|
35
|
+
class SignatureError < Error; end
|
|
36
|
+
|
|
37
|
+
class TransactionError < APIError; end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DkPaymentGateway
|
|
4
|
+
class IntraTransaction
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Intra (DK - DK) Beneficiary Account Inquiry
|
|
12
|
+
# Validates beneficiary account details before initiating a fund transfer
|
|
13
|
+
#
|
|
14
|
+
# @param params [Hash] Account inquiry parameters
|
|
15
|
+
# @option params [String] :request_id Unique identifier for the inquiry request
|
|
16
|
+
# @option params [Numeric] :amount Transaction amount
|
|
17
|
+
# @option params [String] :currency Currency code (e.g., "BTN")
|
|
18
|
+
# @option params [String] :bene_bank_code Beneficiary bank code (1060 for intra)
|
|
19
|
+
# @option params [String] :bene_account_number Beneficiary account number
|
|
20
|
+
# @option params [String] :source_account_name Source account holder name (optional)
|
|
21
|
+
# @option params [String] :source_account_number Source account number
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] Response containing inquiry_id and account_name
|
|
24
|
+
def account_inquiry(params)
|
|
25
|
+
validate_inquiry_params!(params)
|
|
26
|
+
|
|
27
|
+
request_body = build_inquiry_body(params)
|
|
28
|
+
signature_headers = generate_signature_headers(request_body)
|
|
29
|
+
|
|
30
|
+
response = client.post(
|
|
31
|
+
"/v1/beneficiary/account_inquiry",
|
|
32
|
+
body: request_body,
|
|
33
|
+
headers: signature_headers
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
validate_response!(response, "Account Inquiry")
|
|
37
|
+
response["response_data"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Intra (DK - DK) Fund Transfer
|
|
41
|
+
# Initiates a fund transfer after successful account inquiry
|
|
42
|
+
#
|
|
43
|
+
# @param params [Hash] Fund transfer parameters
|
|
44
|
+
# @option params [String] :request_id Unique identifier for the request
|
|
45
|
+
# @option params [String] :inquiry_id Inquiry ID from account_inquiry
|
|
46
|
+
# @option params [String] :source_app Source application identifier
|
|
47
|
+
# @option params [Numeric] :transaction_amount Amount to transfer
|
|
48
|
+
# @option params [String] :currency Currency code (e.g., "BTN")
|
|
49
|
+
# @option params [String] :transaction_datetime Transaction timestamp (ISO 8601)
|
|
50
|
+
# @option params [String] :bene_bank_code Beneficiary bank code
|
|
51
|
+
# @option params [String] :bene_account_number Beneficiary account number
|
|
52
|
+
# @option params [String] :bene_cust_name Beneficiary customer name
|
|
53
|
+
# @option params [String] :source_account_name Source account holder name (optional)
|
|
54
|
+
# @option params [String] :source_account_number Source account number
|
|
55
|
+
# @option params [String] :payment_type Payment type (should be "INTRA" for DK-DK)
|
|
56
|
+
# @option params [String] :narration Transaction description/purpose
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] Response containing inquiry_id and txn_status_id
|
|
59
|
+
def fund_transfer(params)
|
|
60
|
+
validate_transfer_params!(params)
|
|
61
|
+
|
|
62
|
+
request_body = build_transfer_body(params)
|
|
63
|
+
signature_headers = generate_signature_headers(request_body)
|
|
64
|
+
|
|
65
|
+
response = client.post(
|
|
66
|
+
"/v1/initiate/transaction",
|
|
67
|
+
body: request_body,
|
|
68
|
+
headers: signature_headers
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
validate_response!(response, "Fund Transfer")
|
|
72
|
+
response["response_data"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def build_inquiry_body(params)
|
|
78
|
+
{
|
|
79
|
+
request_id: params[:request_id],
|
|
80
|
+
amount: params[:amount].to_s,
|
|
81
|
+
currency: params[:currency],
|
|
82
|
+
bene_bank_code: params[:bene_bank_code],
|
|
83
|
+
bene_account_number: params[:bene_account_number],
|
|
84
|
+
soure_account_number: params[:source_account_number] # Note: API has typo "soure"
|
|
85
|
+
}.tap do |body|
|
|
86
|
+
body[:source_account_name] = params[:source_account_name] if params[:source_account_name]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_transfer_body(params)
|
|
91
|
+
{
|
|
92
|
+
request_id: params[:request_id],
|
|
93
|
+
inquiry_id: params[:inquiry_id],
|
|
94
|
+
transaction_datetime: params[:transaction_datetime],
|
|
95
|
+
source_app: params[:source_app] || client.config.source_app,
|
|
96
|
+
transaction_amount: params[:transaction_amount],
|
|
97
|
+
currency: params[:currency],
|
|
98
|
+
payment_type: params[:payment_type] || "INTRA",
|
|
99
|
+
source_account_number: params[:source_account_number],
|
|
100
|
+
bene_cust_name: params[:bene_cust_name],
|
|
101
|
+
bene_account_number: params[:bene_account_number],
|
|
102
|
+
bene_bank_code: params[:bene_bank_code],
|
|
103
|
+
narration: params[:narration]
|
|
104
|
+
}.tap do |body|
|
|
105
|
+
body[:source_account_name] = params[:source_account_name] if params[:source_account_name]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_inquiry_params!(params)
|
|
110
|
+
required = [:request_id, :amount, :currency, :bene_bank_code,
|
|
111
|
+
:bene_account_number, :source_account_number]
|
|
112
|
+
|
|
113
|
+
missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
|
|
114
|
+
|
|
115
|
+
raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_transfer_params!(params)
|
|
119
|
+
required = [:request_id, :inquiry_id, :transaction_amount, :currency,
|
|
120
|
+
:transaction_datetime, :bene_bank_code, :bene_account_number,
|
|
121
|
+
:bene_cust_name, :source_account_number, :narration]
|
|
122
|
+
|
|
123
|
+
missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
|
|
124
|
+
|
|
125
|
+
raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_response!(response, operation)
|
|
129
|
+
unless response.is_a?(Hash) && response["response_code"] == "0000"
|
|
130
|
+
error_msg = response["response_description"] || response["response_message"] || "Unknown error"
|
|
131
|
+
raise TransactionError.new(
|
|
132
|
+
"#{operation} failed: #{error_msg}",
|
|
133
|
+
response_code: response["response_code"],
|
|
134
|
+
response_message: response["response_message"],
|
|
135
|
+
response_description: response["response_description"]
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def generate_signature_headers(request_body)
|
|
141
|
+
raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
|
|
142
|
+
|
|
143
|
+
Signature.generate(client.private_key, request_body)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DkPaymentGateway
|
|
4
|
+
class PullPayment
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Payment Gateway Authorization (Account inquiry and OTP request)
|
|
12
|
+
#
|
|
13
|
+
# @param params [Hash] Authorization parameters
|
|
14
|
+
# @option params [String] :transaction_datetime Transaction timestamp in UTC (ISO 8601)
|
|
15
|
+
# @option params [String] :stan_number 12-digit unique transaction number
|
|
16
|
+
# @option params [Numeric] :transaction_amount Amount of the transaction
|
|
17
|
+
# @option params [Numeric] :transaction_fee Transaction fee (use 0 if no fee)
|
|
18
|
+
# @option params [String] :payment_desc Payment description
|
|
19
|
+
# @option params [String] :account_number Beneficiary account number
|
|
20
|
+
# @option params [String] :account_name Beneficiary account name
|
|
21
|
+
# @option params [String] :email_id Beneficiary email (optional)
|
|
22
|
+
# @option params [String] :phone_number Beneficiary phone number
|
|
23
|
+
# @option params [String] :remitter_account_number Remitter account number
|
|
24
|
+
# @option params [String] :remitter_account_name Remitter account name
|
|
25
|
+
# @option params [String] :remitter_bank_id Remitter bank identifier
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash] Response containing bfs_txn_id, stan_number, account_number, remitter_account_number
|
|
28
|
+
def authorize(params)
|
|
29
|
+
validate_authorization_params!(params)
|
|
30
|
+
|
|
31
|
+
request_body = build_authorization_body(params)
|
|
32
|
+
signature_headers = generate_signature_headers(request_body)
|
|
33
|
+
|
|
34
|
+
response = client.post(
|
|
35
|
+
"/v1/account_auth/pull-payment",
|
|
36
|
+
body: request_body,
|
|
37
|
+
headers: signature_headers
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
validate_response!(response, "Authorization")
|
|
41
|
+
response["response_data"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Payment Gateway Debit Request
|
|
45
|
+
# Completes a previously authorized payment by verifying OTP
|
|
46
|
+
#
|
|
47
|
+
# @param params [Hash] Debit request parameters
|
|
48
|
+
# @option params [String] :request_id Unique identifier for the request
|
|
49
|
+
# @option params [String] :bfs_txn_id Transaction ID from authorization
|
|
50
|
+
# @option params [String] :bfs_remitter_otp OTP sent to remitter
|
|
51
|
+
# @option params [String] :bfs_order_no Order number (optional)
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash] Response containing bfs_txn_id, code, description
|
|
54
|
+
def debit(params)
|
|
55
|
+
validate_debit_params!(params)
|
|
56
|
+
|
|
57
|
+
request_body = build_debit_body(params)
|
|
58
|
+
signature_headers = generate_signature_headers(request_body)
|
|
59
|
+
|
|
60
|
+
response = client.post(
|
|
61
|
+
"/v1/debit_request/pull-payment",
|
|
62
|
+
body: request_body,
|
|
63
|
+
headers: signature_headers
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
validate_response!(response, "Debit")
|
|
67
|
+
response["response_data"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generate STAN number
|
|
71
|
+
# @param source_app_suffix [String] Last 4 digits of source_app (e.g., "0201")
|
|
72
|
+
# @param transaction_identifier [String] 8-digit identifier (timestamp or counter)
|
|
73
|
+
# @return [String] 12-digit STAN number
|
|
74
|
+
def self.generate_stan(source_app_suffix, transaction_identifier = nil)
|
|
75
|
+
suffix = source_app_suffix.to_s[-4..]
|
|
76
|
+
|
|
77
|
+
identifier = if transaction_identifier
|
|
78
|
+
transaction_identifier.to_s[-8..]
|
|
79
|
+
else
|
|
80
|
+
# Generate from current timestamp (HHMMSSMS format)
|
|
81
|
+
time = Time.now
|
|
82
|
+
format("%02d%02d%02d%02d", time.hour, time.min, time.sec, time.usec / 10000)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
"#{suffix}#{identifier}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def build_authorization_body(params)
|
|
91
|
+
{
|
|
92
|
+
transaction_datetime: params[:transaction_datetime],
|
|
93
|
+
stan_number: params[:stan_number],
|
|
94
|
+
transaction_amount: params[:transaction_amount],
|
|
95
|
+
transaction_fee: params[:transaction_fee] || 0,
|
|
96
|
+
payment_desc: params[:payment_desc],
|
|
97
|
+
account_number: params[:account_number],
|
|
98
|
+
account_name: params[:account_name],
|
|
99
|
+
phone_number: params[:phone_number],
|
|
100
|
+
remitter_account_number: params[:remitter_account_number],
|
|
101
|
+
remitter_account_name: params[:remitter_account_name],
|
|
102
|
+
remitter_bank_id: params[:remitter_bank_id]
|
|
103
|
+
}.tap do |body|
|
|
104
|
+
body[:email_id] = params[:email_id] if params[:email_id]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_debit_body(params)
|
|
109
|
+
{
|
|
110
|
+
request_id: params[:request_id],
|
|
111
|
+
bfs_bfsTxnId: params[:bfs_txn_id],
|
|
112
|
+
bfs_remitter_Otp: params[:bfs_remitter_otp]
|
|
113
|
+
}.tap do |body|
|
|
114
|
+
body[:bfs_orderNo] = params[:bfs_order_no] if params[:bfs_order_no]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_authorization_params!(params)
|
|
119
|
+
required = [:transaction_datetime, :stan_number, :transaction_amount, :payment_desc,
|
|
120
|
+
:account_number, :account_name, :phone_number, :remitter_account_number,
|
|
121
|
+
:remitter_account_name, :remitter_bank_id]
|
|
122
|
+
|
|
123
|
+
missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
|
|
124
|
+
|
|
125
|
+
raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_debit_params!(params)
|
|
129
|
+
required = [:request_id, :bfs_txn_id, :bfs_remitter_otp]
|
|
130
|
+
|
|
131
|
+
missing = required.select { |key| params[key].nil? || params[key].to_s.empty? }
|
|
132
|
+
|
|
133
|
+
raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_response!(response, operation)
|
|
137
|
+
unless response.is_a?(Hash) && response["response_code"] == "0000"
|
|
138
|
+
error_msg = response["response_description"] || response["response_message"] || "Unknown error"
|
|
139
|
+
raise TransactionError.new(
|
|
140
|
+
"#{operation} failed: #{error_msg}",
|
|
141
|
+
response_code: response["response_code"],
|
|
142
|
+
response_message: response["response_message"],
|
|
143
|
+
response_description: response["response_description"]
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def generate_signature_headers(request_body)
|
|
149
|
+
raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
|
|
150
|
+
|
|
151
|
+
Signature.generate(client.private_key, request_body)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DkPaymentGateway
|
|
4
|
+
class QrPayment
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Generate QR Code for payment transaction
|
|
12
|
+
#
|
|
13
|
+
# If amount = 0, generates a Static QR (payer enters amount)
|
|
14
|
+
# If amount > 0, generates a Dynamic QR (amount is fixed)
|
|
15
|
+
#
|
|
16
|
+
# @param params [Hash] QR generation parameters
|
|
17
|
+
# @option params [String] :request_id Unique identifier for the request
|
|
18
|
+
# @option params [String] :currency Currency code (e.g., "BTN")
|
|
19
|
+
# @option params [String] :bene_account_number Beneficiary account number
|
|
20
|
+
# @option params [Numeric] :amount Transaction amount (0 for static QR, >0 for dynamic QR)
|
|
21
|
+
# @option params [String] :mcc_code Merchant Category Code (ISO 18245)
|
|
22
|
+
# @option params [String] :remarks Optional notes or comments (optional)
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash] Response containing base64 encoded QR image
|
|
25
|
+
def generate_qr(params)
|
|
26
|
+
validate_qr_params!(params)
|
|
27
|
+
|
|
28
|
+
request_body = build_qr_body(params)
|
|
29
|
+
signature_headers = generate_signature_headers(request_body)
|
|
30
|
+
|
|
31
|
+
response = client.post(
|
|
32
|
+
"/v1/generate_qr",
|
|
33
|
+
body: request_body,
|
|
34
|
+
headers: signature_headers
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
validate_response!(response, "QR Generation")
|
|
38
|
+
response["response_data"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Decode base64 QR image and save to file
|
|
42
|
+
# @param base64_image [String] Base64 encoded image
|
|
43
|
+
# @param file_path [String] Path to save the image
|
|
44
|
+
def save_qr_image(base64_image, file_path)
|
|
45
|
+
require "base64"
|
|
46
|
+
|
|
47
|
+
image_data = Base64.decode64(base64_image)
|
|
48
|
+
File.open(file_path, "wb") { |f| f.write(image_data) }
|
|
49
|
+
|
|
50
|
+
file_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_qr_body(params)
|
|
56
|
+
{
|
|
57
|
+
request_id: params[:request_id],
|
|
58
|
+
currency: params[:currency],
|
|
59
|
+
bene_account_number: params[:bene_account_number],
|
|
60
|
+
amount: params[:amount],
|
|
61
|
+
mcc_code: params[:mcc_code]
|
|
62
|
+
}.tap do |body|
|
|
63
|
+
body[:remarks] = params[:remarks] if params[:remarks]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_qr_params!(params)
|
|
68
|
+
required = [:request_id, :currency, :bene_account_number, :amount, :mcc_code]
|
|
69
|
+
|
|
70
|
+
missing = required.select { |key| params[key].nil? }
|
|
71
|
+
|
|
72
|
+
raise InvalidParameterError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
73
|
+
|
|
74
|
+
# Validate amount is numeric
|
|
75
|
+
unless params[:amount].is_a?(Numeric) || params[:amount].to_s.match?(/^\d+(\.\d+)?$/)
|
|
76
|
+
raise InvalidParameterError, "Amount must be a valid number"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_response!(response, operation)
|
|
81
|
+
unless response.is_a?(Hash) && response["response_code"] == "0000"
|
|
82
|
+
error_msg = response["response_detail"] || response["response_message"] || "Unknown error"
|
|
83
|
+
raise TransactionError.new(
|
|
84
|
+
"#{operation} failed: #{error_msg}",
|
|
85
|
+
response_code: response["response_code"],
|
|
86
|
+
response_detail: response["response_detail"]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def generate_signature_headers(request_body)
|
|
92
|
+
raise SignatureError, "Private key not available. Call client.authenticate! first" unless client.private_key
|
|
93
|
+
|
|
94
|
+
Signature.generate(client.private_key, request_body)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jwt"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
|
|
9
|
+
module DkPaymentGateway
|
|
10
|
+
class Signature
|
|
11
|
+
attr_reader :private_key
|
|
12
|
+
|
|
13
|
+
def initialize(private_key)
|
|
14
|
+
@private_key = private_key
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate signature headers for a request
|
|
18
|
+
# Returns a hash with DK-Signature, DK-Timestamp, and DK-Nonce
|
|
19
|
+
def generate_headers(request_body)
|
|
20
|
+
timestamp = generate_timestamp
|
|
21
|
+
nonce = generate_nonce
|
|
22
|
+
signature = sign_request(request_body, timestamp, nonce)
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
"DK-Signature" => "DKSignature #{signature}",
|
|
26
|
+
"DK-Timestamp" => timestamp,
|
|
27
|
+
"DK-Nonce" => nonce
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Generate ISO 8601 timestamp
|
|
34
|
+
def generate_timestamp
|
|
35
|
+
Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Generate unique alphanumeric nonce
|
|
39
|
+
def generate_nonce
|
|
40
|
+
SecureRandom.hex(16)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Sign the request using RS256 algorithm
|
|
44
|
+
def sign_request(request_body, timestamp, nonce)
|
|
45
|
+
# Serialize request body to canonical JSON (sorted keys, no spaces)
|
|
46
|
+
request_body_str = JSON.generate(request_body, space: "", object_nl: "", array_nl: "")
|
|
47
|
+
|
|
48
|
+
# Base64 encode the request body
|
|
49
|
+
body_base64 = Base64.strict_encode64(request_body_str)
|
|
50
|
+
|
|
51
|
+
# Create the payload for signing
|
|
52
|
+
token_payload = {
|
|
53
|
+
"data" => body_base64,
|
|
54
|
+
"timestamp" => timestamp,
|
|
55
|
+
"nonce" => nonce
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Sign using RS256
|
|
59
|
+
JWT.encode(token_payload, OpenSSL::PKey::RSA.new(private_key), "RS256")
|
|
60
|
+
rescue => e
|
|
61
|
+
raise SignatureError, "Failed to generate signature: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Convenience method to generate signature headers
|
|
66
|
+
def generate(private_key, request_body)
|
|
67
|
+
new(private_key).generate_headers(request_body)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|