dpay 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/CHANGELOG.md +29 -0
- data/LICENSE +201 -0
- data/README.md +191 -0
- data/lib/dpay/bank/bank.rb +28 -0
- data/lib/dpay/bank/bank_service.rb +28 -0
- data/lib/dpay/blik/blik_alias.rb +27 -0
- data/lib/dpay/blik/blik_alias_registration.rb +21 -0
- data/lib/dpay/blik/blik_alias_type.rb +18 -0
- data/lib/dpay/blik/blik_app.rb +17 -0
- data/lib/dpay/blik/blik_recurring_registration.rb +74 -0
- data/lib/dpay/blik/blik_recurring_registration_info.rb +26 -0
- data/lib/dpay/blik/blik_recurring_status.rb +28 -0
- data/lib/dpay/blik/blik_service.rb +51 -0
- data/lib/dpay/card/apple_pay_request.rb +38 -0
- data/lib/dpay/card/card_data.rb +30 -0
- data/lib/dpay/card/card_encryptor.rb +27 -0
- data/lib/dpay/card/card_payment_request.rb +59 -0
- data/lib/dpay/card/card_payment_result.rb +58 -0
- data/lib/dpay/card/card_recurring_frequency.rb +23 -0
- data/lib/dpay/card/card_recurring_operation.rb +19 -0
- data/lib/dpay/card/card_recurring_registration.rb +69 -0
- data/lib/dpay/card/card_service.rb +47 -0
- data/lib/dpay/card/dcc_decision.rb +18 -0
- data/lib/dpay/card/dcc_markup.rb +18 -0
- data/lib/dpay/card/dcc_offer.rb +36 -0
- data/lib/dpay/card/google_pay_request.rb +35 -0
- data/lib/dpay/card/redirect_type.rb +20 -0
- data/lib/dpay/client.rb +26 -0
- data/lib/dpay/config.rb +39 -0
- data/lib/dpay/currency.rb +19 -0
- data/lib/dpay/errors.rb +77 -0
- data/lib/dpay/http/client.rb +11 -0
- data/lib/dpay/http/net_http_client.rb +52 -0
- data/lib/dpay/http/request.rb +17 -0
- data/lib/dpay/http/response.rb +29 -0
- data/lib/dpay/internal/api_requestor.rb +65 -0
- data/lib/dpay/internal/base_urls.rb +38 -0
- data/lib/dpay/internal/checksum_calculator.rb +26 -0
- data/lib/dpay/internal/coerce.rb +46 -0
- data/lib/dpay/internal/error_mapper.rb +90 -0
- data/lib/dpay/internal/php.rb +71 -0
- data/lib/dpay/ipn/ipn_event.rb +84 -0
- data/lib/dpay/ipn/ipn_type.rb +19 -0
- data/lib/dpay/ipn/ipn_verifier.rb +57 -0
- data/lib/dpay/money.rb +85 -0
- data/lib/dpay/payment/device_info.rb +42 -0
- data/lib/dpay/payment/invoice_details.rb +48 -0
- data/lib/dpay/payment/payer.rb +26 -0
- data/lib/dpay/payment/payment_service.rb +35 -0
- data/lib/dpay/payment/payout_fee_mode.rb +18 -0
- data/lib/dpay/payment/payout_instruction.rb +26 -0
- data/lib/dpay/payment/payout_position.rb +23 -0
- data/lib/dpay/payment/register_payment_request.rb +246 -0
- data/lib/dpay/payment/registered_payment.rb +42 -0
- data/lib/dpay/payment/return_urls.rb +20 -0
- data/lib/dpay/payment/transaction.rb +54 -0
- data/lib/dpay/payment/transaction_refund.rb +21 -0
- data/lib/dpay/payment/transaction_status.rb +21 -0
- data/lib/dpay/payment/transaction_type.rb +23 -0
- data/lib/dpay/payout/payout_details.rb +50 -0
- data/lib/dpay/payout/payout_receiver.rb +22 -0
- data/lib/dpay/payout/payout_service.rb +23 -0
- data/lib/dpay/refund/refund.rb +22 -0
- data/lib/dpay/refund/refund_availability.rb +23 -0
- data/lib/dpay/refund/refund_service.rb +46 -0
- data/lib/dpay/testing/mock_http_client.rb +51 -0
- data/lib/dpay/testing.rb +4 -0
- data/lib/dpay/version.rb +5 -0
- data/lib/dpay.rb +68 -0
- data/sig/dpay/bank/bank.rbs +17 -0
- data/sig/dpay/bank/bank_service.rbs +13 -0
- data/sig/dpay/blik/blik_alias.rbs +17 -0
- data/sig/dpay/blik/blik_alias_registration.rbs +6 -0
- data/sig/dpay/blik/blik_alias_type.rbs +11 -0
- data/sig/dpay/blik/blik_app.rbs +10 -0
- data/sig/dpay/blik/blik_recurring_registration.rbs +24 -0
- data/sig/dpay/blik/blik_recurring_registration_info.rbs +17 -0
- data/sig/dpay/blik/blik_recurring_status.rbs +17 -0
- data/sig/dpay/blik/blik_service.rbs +13 -0
- data/sig/dpay/card/apple_pay_request.rbs +13 -0
- data/sig/dpay/card/card_data.rbs +15 -0
- data/sig/dpay/card/card_encryptor.rbs +9 -0
- data/sig/dpay/card/card_payment_request.rbs +17 -0
- data/sig/dpay/card/card_payment_result.rbs +22 -0
- data/sig/dpay/card/card_recurring_frequency.rbs +16 -0
- data/sig/dpay/card/card_recurring_operation.rbs +12 -0
- data/sig/dpay/card/card_recurring_registration.rbs +22 -0
- data/sig/dpay/card/card_service.rbs +16 -0
- data/sig/dpay/card/dcc_decision.rbs +11 -0
- data/sig/dpay/card/dcc_markup.rbs +10 -0
- data/sig/dpay/card/dcc_offer.rbs +21 -0
- data/sig/dpay/card/google_pay_request.rbs +12 -0
- data/sig/dpay/card/redirect_type.rbs +13 -0
- data/sig/dpay/client.rbs +21 -0
- data/sig/dpay/config.rbs +25 -0
- data/sig/dpay/currency.rbs +11 -0
- data/sig/dpay/errors.rbs +86 -0
- data/sig/dpay/http/client.rbs +7 -0
- data/sig/dpay/http/net_http_client.rbs +15 -0
- data/sig/dpay/http/request.rbs +12 -0
- data/sig/dpay/http/response.rbs +15 -0
- data/sig/dpay/internal/api_requestor.rbs +23 -0
- data/sig/dpay/internal/base_urls.rbs +13 -0
- data/sig/dpay/internal/checksum_calculator.rbs +9 -0
- data/sig/dpay/internal/coerce.rbs +12 -0
- data/sig/dpay/internal/error_mapper.rbs +16 -0
- data/sig/dpay/internal/php.rbs +16 -0
- data/sig/dpay/ipn/ipn_event.rbs +28 -0
- data/sig/dpay/ipn/ipn_type.rbs +12 -0
- data/sig/dpay/ipn/ipn_verifier.rbs +11 -0
- data/sig/dpay/money.rbs +25 -0
- data/sig/dpay/payment/device_info.rbs +25 -0
- data/sig/dpay/payment/invoice_details.rbs +17 -0
- data/sig/dpay/payment/payer.rbs +14 -0
- data/sig/dpay/payment/payment_service.rbs +11 -0
- data/sig/dpay/payment/payout_fee_mode.rbs +11 -0
- data/sig/dpay/payment/payout_instruction.rbs +8 -0
- data/sig/dpay/payment/payout_position.rbs +10 -0
- data/sig/dpay/payment/register_payment_request.rbs +56 -0
- data/sig/dpay/payment/registered_payment.rbs +20 -0
- data/sig/dpay/payment/return_urls.rbs +11 -0
- data/sig/dpay/payment/transaction.rbs +25 -0
- data/sig/dpay/payment/transaction_refund.rbs +14 -0
- data/sig/dpay/payment/transaction_status.rbs +14 -0
- data/sig/dpay/payment/transaction_type.rbs +16 -0
- data/sig/dpay/payout/payout_details.rbs +24 -0
- data/sig/dpay/payout/payout_receiver.rbs +15 -0
- data/sig/dpay/payout/payout_service.rbs +6 -0
- data/sig/dpay/refund/refund.rbs +11 -0
- data/sig/dpay/refund/refund_availability.rbs +12 -0
- data/sig/dpay/refund/refund_service.rbs +14 -0
- data/sig/dpay/testing/mock_http_client.rbs +16 -0
- data/sig/dpay/testing.rbs +4 -0
- data/sig/dpay/version.rbs +3 -0
- data/sig/dpay.rbs +2 -0
- metadata +180 -0
data/lib/dpay/errors.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module Error; end
|
|
5
|
+
|
|
6
|
+
class InvalidArgumentError < ArgumentError
|
|
7
|
+
include Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class TransportError < StandardError
|
|
11
|
+
include Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class SignatureVerificationError < StandardError
|
|
15
|
+
include Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class CardEncryptionError < StandardError
|
|
19
|
+
include Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class ApiError < StandardError
|
|
23
|
+
include Error
|
|
24
|
+
|
|
25
|
+
attr_reader :http_status, :error_code, :field_errors, :raw_body
|
|
26
|
+
|
|
27
|
+
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "")
|
|
28
|
+
super(message)
|
|
29
|
+
@http_status = http_status
|
|
30
|
+
@error_code = error_code
|
|
31
|
+
@field_errors = field_errors
|
|
32
|
+
@raw_body = raw_body
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class AuthenticationError < ApiError; end
|
|
37
|
+
class InvalidRequestError < ApiError; end
|
|
38
|
+
class AccessDeniedError < ApiError; end
|
|
39
|
+
class NotFoundError < ApiError; end
|
|
40
|
+
class ApiServerError < ApiError; end
|
|
41
|
+
|
|
42
|
+
class RateLimitError < ApiError
|
|
43
|
+
attr_reader :retry_after, :limit, :remaining
|
|
44
|
+
|
|
45
|
+
def initialize(message, http_status, retry_after = nil, limit = nil, remaining = nil, raw_body = "")
|
|
46
|
+
super(message, http_status, nil, {}, raw_body)
|
|
47
|
+
@retry_after = retry_after
|
|
48
|
+
@limit = limit
|
|
49
|
+
@remaining = remaining
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class PaymentRejectedError < ApiError
|
|
54
|
+
attr_reader :transaction_id
|
|
55
|
+
|
|
56
|
+
def self.from_api(data)
|
|
57
|
+
message = %w[message msg].filter_map { |key| data[key] if data[key].is_a?(String) }.first || "Payment rejected"
|
|
58
|
+
error_code = data["errorcode"].is_a?(String) ? data["errorcode"] : nil
|
|
59
|
+
transaction_id = data["transactionId"].nil? ? nil : Internal::PHP.strval(data["transactionId"])
|
|
60
|
+
|
|
61
|
+
new(message, 200, error_code, {}, Internal::PHP.json_encode(data), transaction_id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "", transaction_id = nil)
|
|
65
|
+
super(message, http_status, error_code, field_errors, raw_body)
|
|
66
|
+
@transaction_id = transaction_id
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class CardPaymentError < ApiError
|
|
71
|
+
def self.from_api(data)
|
|
72
|
+
message = data["message"].is_a?(String) && !data["message"].empty? ? data["message"] : "Card payment rejected"
|
|
73
|
+
|
|
74
|
+
new(message, 200, message, {}, Internal::PHP.json_encode(data))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module DPay
|
|
8
|
+
module HTTP
|
|
9
|
+
class NetHTTPClient < Client
|
|
10
|
+
CONNECT_TIMEOUT = 10
|
|
11
|
+
|
|
12
|
+
NETWORK_ERRORS = [
|
|
13
|
+
Timeout::Error, SocketError, IOError, SystemCallError, OpenSSL::SSL::SSLError, Net::HTTPBadResponse
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(timeout = 30)
|
|
17
|
+
super()
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def request(api_request)
|
|
22
|
+
uri = URI.parse(api_request.url)
|
|
23
|
+
raise TransportError, "Unsupported URL scheme: #{api_request.url}" unless uri.is_a?(URI::HTTP)
|
|
24
|
+
|
|
25
|
+
host = uri.host
|
|
26
|
+
raise TransportError, "URL has no host: #{api_request.url}" if host.nil?
|
|
27
|
+
|
|
28
|
+
http = Net::HTTP.new(host, uri.port)
|
|
29
|
+
http.use_ssl = uri.scheme == "https"
|
|
30
|
+
http.open_timeout = CONNECT_TIMEOUT
|
|
31
|
+
http.read_timeout = @timeout
|
|
32
|
+
http.write_timeout = @timeout
|
|
33
|
+
|
|
34
|
+
response = http.request(build_request(uri, api_request))
|
|
35
|
+
|
|
36
|
+
Response.new(response.code.to_i, response.each_header.to_h, response.body.to_s)
|
|
37
|
+
rescue *NETWORK_ERRORS => e
|
|
38
|
+
raise TransportError, "HTTP transport error: #{e.class}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_request(uri, api_request)
|
|
44
|
+
klass = Net::HTTP.const_get(api_request.method.to_s.capitalize)
|
|
45
|
+
request = klass.new(uri.request_uri)
|
|
46
|
+
api_request.headers.each { |name, value| request[name] = value }
|
|
47
|
+
request.body = api_request.body unless api_request.body.nil?
|
|
48
|
+
request
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module HTTP
|
|
5
|
+
class Request
|
|
6
|
+
attr_reader :method, :url, :headers, :body
|
|
7
|
+
|
|
8
|
+
def initialize(method, url, headers = {}, body = nil)
|
|
9
|
+
@method = method
|
|
10
|
+
@url = url
|
|
11
|
+
@headers = headers.freeze
|
|
12
|
+
@body = body
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module DPay
|
|
6
|
+
module HTTP
|
|
7
|
+
class Response
|
|
8
|
+
attr_reader :status, :headers, :body
|
|
9
|
+
|
|
10
|
+
def initialize(status, headers, body)
|
|
11
|
+
@status = status
|
|
12
|
+
@headers = headers.each_with_object({}) { |(name, value), result| result[name.to_s.downcase] = value }.freeze
|
|
13
|
+
@body = body
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def header(name)
|
|
18
|
+
@headers[name.to_s.downcase]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def decode_json
|
|
22
|
+
decoded = JSON.parse(@body.to_s)
|
|
23
|
+
decoded.is_a?(Hash) || decoded.is_a?(Array) ? decoded : nil
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module Internal
|
|
5
|
+
class ApiRequestor
|
|
6
|
+
USER_AGENT = "dpay-ruby-sdk/#{VERSION} ruby/#{RUBY_VERSION}".freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :config, :checksum
|
|
9
|
+
|
|
10
|
+
def initialize(config, http_client)
|
|
11
|
+
@config = config
|
|
12
|
+
@http_client = http_client
|
|
13
|
+
@checksum = ChecksumCalculator.new(config.secret_hash)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def service
|
|
17
|
+
@config.service
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def post_json(host, path, body)
|
|
21
|
+
decode(send_request("POST", host, path, body))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_json(host, path)
|
|
25
|
+
decode(send_request("GET", host, path, nil))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get_text(host, path)
|
|
29
|
+
send_request("GET", host, path, nil).body
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def send_request(method, host, path, body)
|
|
33
|
+
response = send_raw(method, host, path, body)
|
|
34
|
+
raise map_error(response) if response.status >= 400
|
|
35
|
+
|
|
36
|
+
response
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_raw(method, host, path, body)
|
|
40
|
+
headers = { "Accept" => "application/json", "User-Agent" => USER_AGENT }
|
|
41
|
+
encoded = nil
|
|
42
|
+
|
|
43
|
+
unless body.nil?
|
|
44
|
+
headers["Content-Type"] = "application/json"
|
|
45
|
+
encoded = PHP.json_encode(body)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@http_client.request(HTTP::Request.new(method, @config.base_urls.resolve(host) + path, headers, encoded))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def map_error(response)
|
|
52
|
+
ErrorMapper.map(response)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def decode(response)
|
|
58
|
+
decoded = response.decode_json
|
|
59
|
+
return decoded unless decoded.nil?
|
|
60
|
+
|
|
61
|
+
raise ApiServerError.new("Invalid JSON in API response", response.status, nil, {}, response.body)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module Internal
|
|
5
|
+
class BaseUrls
|
|
6
|
+
API_PAYMENTS = :api_payments
|
|
7
|
+
PANEL = :panel
|
|
8
|
+
GATEWAY = :gateway
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
API_PAYMENTS => "https://api-payments.dpay.pl",
|
|
12
|
+
PANEL => "https://panel.dpay.pl",
|
|
13
|
+
GATEWAY => "https://secure.dpay.pl"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(overrides = {})
|
|
17
|
+
urls = DEFAULTS.dup
|
|
18
|
+
|
|
19
|
+
overrides.each do |host, url|
|
|
20
|
+
key = host.to_sym
|
|
21
|
+
raise InvalidArgumentError, %(Unknown base URL key "#{host}") unless DEFAULTS.key?(key)
|
|
22
|
+
|
|
23
|
+
urls[key] = url.to_s.sub(%r{/+\z}, "")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@urls = urls.freeze
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resolve(host)
|
|
31
|
+
key = host.to_sym
|
|
32
|
+
raise InvalidArgumentError, %(Unknown API host "#{host}") unless @urls.key?(key)
|
|
33
|
+
|
|
34
|
+
@urls[key]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module DPay
|
|
6
|
+
module Internal
|
|
7
|
+
class ChecksumCalculator
|
|
8
|
+
def initialize(secret_hash)
|
|
9
|
+
@secret_hash = secret_hash
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def secret_second(service, fields)
|
|
14
|
+
parts = [service, @secret_hash] + fields.map { |field| PHP.strval(field) }
|
|
15
|
+
|
|
16
|
+
Digest::SHA256.hexdigest(parts.join("|"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ordered_body(values)
|
|
20
|
+
joined = values.map { |value| PHP.strval(value) }.join("|")
|
|
21
|
+
|
|
22
|
+
Digest::SHA256.hexdigest("#{joined}|#{@secret_hash}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module Internal
|
|
5
|
+
module Coerce
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def string(value)
|
|
9
|
+
return nil unless value.is_a?(String) || value.is_a?(Numeric) ||
|
|
10
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
11
|
+
|
|
12
|
+
PHP.strval(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def money(value, currency = Currency::PLN)
|
|
16
|
+
Money.try_from_api_number(value.nil? ? 0 : value, currency) || Money.pln(0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def boolean(value)
|
|
20
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
21
|
+
return value != 0 if value.is_a?(Numeric)
|
|
22
|
+
return !value.empty? && value != "0" if value.is_a?(String)
|
|
23
|
+
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def integer(value)
|
|
28
|
+
return value.to_i if value.is_a?(Numeric)
|
|
29
|
+
return value.to_i if value.is_a?(String) && /\A\s*[+-]?\d+/.match?(value)
|
|
30
|
+
|
|
31
|
+
0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def optional_integer(value)
|
|
35
|
+
return value.to_i if value.is_a?(Numeric)
|
|
36
|
+
return value.to_i if value.is_a?(String) && /\A\s*[+-]?\d+\z/.match?(value)
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def list(value)
|
|
42
|
+
value.is_a?(Array) ? value.grep(Hash) : []
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module Internal
|
|
5
|
+
module ErrorMapper
|
|
6
|
+
DEFAULT_MESSAGE = "Unexpected API error"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def map(response)
|
|
11
|
+
decoded = response.decode_json
|
|
12
|
+
data = decoded.is_a?(Hash) ? decoded : {}
|
|
13
|
+
message = extract_message(data)
|
|
14
|
+
|
|
15
|
+
return rate_limit(response, message) if response.status == 429
|
|
16
|
+
|
|
17
|
+
error_class(response.status).new(
|
|
18
|
+
message,
|
|
19
|
+
response.status,
|
|
20
|
+
data["errorcode"].is_a?(String) ? data["errorcode"] : nil,
|
|
21
|
+
normalize_field_errors(data["errors"]),
|
|
22
|
+
response.body
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error_class(status)
|
|
27
|
+
case status
|
|
28
|
+
when 401 then AuthenticationError
|
|
29
|
+
when 403 then AccessDeniedError
|
|
30
|
+
when 404 then NotFoundError
|
|
31
|
+
when 400, 422 then InvalidRequestError
|
|
32
|
+
else status >= 500 ? ApiServerError : ApiError
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_message(data)
|
|
37
|
+
return data["message"] if data["message"].is_a?(String)
|
|
38
|
+
return data["msg"] if data["msg"].is_a?(String)
|
|
39
|
+
|
|
40
|
+
DEFAULT_MESSAGE
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rate_limit(response, message)
|
|
44
|
+
RateLimitError.new(
|
|
45
|
+
message,
|
|
46
|
+
response.status,
|
|
47
|
+
int_header(response, "Retry-After"),
|
|
48
|
+
int_header(response, "X-RateLimit-Limit"),
|
|
49
|
+
int_header(response, "X-RateLimit-Remaining"),
|
|
50
|
+
response.body
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def normalize_field_errors(errors)
|
|
55
|
+
pairs = case errors
|
|
56
|
+
when Array then errors.each_with_index.map { |messages, index| [index, messages] }
|
|
57
|
+
when Hash then errors.to_a
|
|
58
|
+
else return {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
pairs.each_with_object({}) do |(field, messages), result|
|
|
62
|
+
normalized = normalize_messages(messages)
|
|
63
|
+
result[PHP.strval(field)] = normalized unless normalized.nil?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_messages(messages)
|
|
68
|
+
case messages
|
|
69
|
+
when String then [messages]
|
|
70
|
+
when Array then messages.select { |message| scalar?(message) }.map { |message| PHP.strval(message) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def scalar?(value)
|
|
75
|
+
value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def int_header(response, name)
|
|
79
|
+
value = response.header(name)
|
|
80
|
+
return nil if value.nil?
|
|
81
|
+
|
|
82
|
+
match = /\A\s*[+-]?\d+/.match(value.to_s)
|
|
83
|
+
return 0 if match.nil?
|
|
84
|
+
|
|
85
|
+
digits = match[0]
|
|
86
|
+
digits.nil? ? 0 : digits.strip.to_i
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module DPay
|
|
6
|
+
module Internal
|
|
7
|
+
module PHP
|
|
8
|
+
SAFE_INTEGRAL_FLOAT = 2**53
|
|
9
|
+
NON_UNRESERVED = /[^A-Za-z0-9\-_.~]/n
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def strval(value)
|
|
14
|
+
case value
|
|
15
|
+
when nil, false then ""
|
|
16
|
+
when true then "1"
|
|
17
|
+
when Float then float_to_string(value)
|
|
18
|
+
when String then value
|
|
19
|
+
else value.to_s
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def round(value)
|
|
24
|
+
value >= 0 ? (value + 0.5).floor : (value - 0.5).ceil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def json_encode(data, escape_slashes: false)
|
|
28
|
+
encoded = JSON.generate(normalize_floats(data))
|
|
29
|
+
escape_slashes ? encoded.gsub("/") { "\\/" } : encoded
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raw_url_encode(value)
|
|
33
|
+
value.to_s.b.gsub(NON_UNRESERVED) { |byte| format("%%%02X", byte.ord) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def float_to_string(value)
|
|
37
|
+
return "NAN" if value.nan?
|
|
38
|
+
return value.positive? ? "INF" : "-INF" if value.infinite?
|
|
39
|
+
|
|
40
|
+
formatted = format("%.14G", value)
|
|
41
|
+
return formatted unless formatted.include?("E")
|
|
42
|
+
|
|
43
|
+
parts = formatted.split("E")
|
|
44
|
+
mantissa = parts.fetch(0)
|
|
45
|
+
exponent = parts.fetch(1)
|
|
46
|
+
mantissa += ".0" unless mantissa.include?(".")
|
|
47
|
+
sign = exponent.start_with?("-") ? "-" : "+"
|
|
48
|
+
digits = exponent.sub(/\A[+-]/, "").sub(/\A0+/, "")
|
|
49
|
+
digits = "0" if digits.empty?
|
|
50
|
+
"#{mantissa}E#{sign}#{digits}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_floats(value)
|
|
54
|
+
case value
|
|
55
|
+
when Float
|
|
56
|
+
integral_float?(value) ? value.to_i : value
|
|
57
|
+
when Hash
|
|
58
|
+
value.each_with_object({}) { |(key, item), result| result[key] = normalize_floats(item) }
|
|
59
|
+
when Array
|
|
60
|
+
value.map { |item| normalize_floats(item) }
|
|
61
|
+
else
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def integral_float?(value)
|
|
67
|
+
value.finite? && value.abs < SAFE_INTEGRAL_FLOAT && value.to_i == value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
class IpnEvent
|
|
5
|
+
ACK = "OK"
|
|
6
|
+
|
|
7
|
+
attr_reader :raw
|
|
8
|
+
|
|
9
|
+
def self.from_verified_payload(payload)
|
|
10
|
+
new(payload)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(payload)
|
|
14
|
+
@raw = payload
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def id
|
|
19
|
+
scalar("id") || ""
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def amount
|
|
23
|
+
scalar("amount") || ""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def type
|
|
27
|
+
scalar("type") || ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def signature
|
|
31
|
+
scalar("signature") || ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def email
|
|
35
|
+
scalar("email")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def custom
|
|
39
|
+
scalar("custom")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def capture_payment_id
|
|
43
|
+
scalar("capture_payment_id")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def attempt
|
|
47
|
+
integer("attempt")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def version
|
|
51
|
+
integer("version")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def transfer?
|
|
55
|
+
type == IpnType::TRANSFER
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def capture?
|
|
59
|
+
type == IpnType::CAPTURE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dcb?
|
|
63
|
+
type == IpnType::DCB
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def scalar(key)
|
|
69
|
+
value = @raw[key]
|
|
70
|
+
return nil unless value.is_a?(String) || value.is_a?(Numeric) ||
|
|
71
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
72
|
+
|
|
73
|
+
Internal::PHP.strval(value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def integer(key)
|
|
77
|
+
value = @raw[key]
|
|
78
|
+
return value.to_i if value.is_a?(Integer) || value.is_a?(Float)
|
|
79
|
+
return value.to_i if value.is_a?(String) && /\A[+-]?\d+\z/.match?(value)
|
|
80
|
+
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DPay
|
|
4
|
+
module IpnType
|
|
5
|
+
TRANSFER = "transfer"
|
|
6
|
+
CAPTURE = "capture"
|
|
7
|
+
DCB = "dcb"
|
|
8
|
+
|
|
9
|
+
ALL = [TRANSFER, CAPTURE, DCB].freeze
|
|
10
|
+
|
|
11
|
+
def self.valid?(value)
|
|
12
|
+
ALL.include?(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.assert_valid(value)
|
|
16
|
+
raise InvalidArgumentError, %(Invalid IPN type "#{value}") unless valid?(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
module DPay
|
|
8
|
+
module IpnVerifier
|
|
9
|
+
REQUIRED_FIELDS = %w[id amount type attempt version signature].freeze
|
|
10
|
+
INVALID_PAYLOAD = "Invalid IPN payload"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def construct_event(raw_body, secret_hash)
|
|
15
|
+
payload = parse(raw_body)
|
|
16
|
+
signature = payload["signature"]
|
|
17
|
+
raise SignatureVerificationError, INVALID_PAYLOAD unless signature.is_a?(String)
|
|
18
|
+
|
|
19
|
+
expected = expected_signature(payload, secret_hash)
|
|
20
|
+
raise SignatureVerificationError, "Invalid IPN signature" unless secure_compare(expected, signature)
|
|
21
|
+
|
|
22
|
+
IpnEvent.from_verified_payload(payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse(raw_body)
|
|
26
|
+
payload = begin
|
|
27
|
+
JSON.parse(raw_body.to_s)
|
|
28
|
+
rescue JSON::ParserError
|
|
29
|
+
raise SignatureVerificationError, INVALID_PAYLOAD
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
raise SignatureVerificationError, INVALID_PAYLOAD unless payload.is_a?(Hash)
|
|
33
|
+
raise SignatureVerificationError, INVALID_PAYLOAD unless REQUIRED_FIELDS.all? { |f| !payload[f].nil? }
|
|
34
|
+
|
|
35
|
+
payload
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def expected_signature(payload, secret_hash)
|
|
39
|
+
type = Internal::PHP.strval(payload["type"])
|
|
40
|
+
|
|
41
|
+
parts = [Internal::PHP.strval(payload["id"]), secret_hash, Internal::PHP.strval(payload["amount"])]
|
|
42
|
+
parts << Internal::PHP.strval(payload["email"]) unless type == IpnType::DCB
|
|
43
|
+
parts << type
|
|
44
|
+
parts << Internal::PHP.strval(payload["attempt"])
|
|
45
|
+
parts << Internal::PHP.strval(payload["version"])
|
|
46
|
+
parts << Internal::PHP.strval(payload["custom"])
|
|
47
|
+
|
|
48
|
+
Digest::SHA256.hexdigest(parts.join)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def secure_compare(expected, actual)
|
|
52
|
+
expected.bytesize == actual.bytesize && OpenSSL.fixed_length_secure_compare(expected, actual)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_class_method :parse, :expected_signature, :secure_compare
|
|
56
|
+
end
|
|
57
|
+
end
|