przelewy24_payment 0.2.0 → 2.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 +5 -5
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +17 -18
- data/README.md +345 -139
- data/app/controllers/przelewy24_payment/webhooks_controller.rb +63 -0
- data/app/jobs/przelewy24_payment/application_job.rb +6 -0
- data/app/jobs/przelewy24_payment/charge_due_subscriptions_job.rb +13 -0
- data/app/models/przelewy24_payment/application_record.rb +7 -0
- data/app/models/przelewy24_payment/payment.rb +177 -0
- data/app/models/przelewy24_payment/refund.rb +33 -0
- data/app/models/przelewy24_payment/subscription.rb +147 -0
- data/config/routes.rb +6 -0
- data/lib/generators/przelewy24_payment/install/install_generator.rb +60 -5
- data/lib/generators/przelewy24_payment/install/templates/README +14 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_payments.rb.tt +21 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_refunds.rb.tt +14 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_subscriptions.rb.tt +28 -0
- data/lib/generators/przelewy24_payment/install/templates/initializer.rb.tt +47 -0
- data/lib/przelewy24_payment/api/base.rb +17 -0
- data/lib/przelewy24_payment/api/blik.rb +41 -0
- data/lib/przelewy24_payment/api/cards.rb +26 -0
- data/lib/przelewy24_payment/api/payment_methods.rb +11 -0
- data/lib/przelewy24_payment/api/refunds.rb +25 -0
- data/lib/przelewy24_payment/api/transactions.rb +51 -0
- data/lib/przelewy24_payment/client.rb +66 -0
- data/lib/przelewy24_payment/configuration.rb +81 -0
- data/lib/przelewy24_payment/engine.rb +12 -1
- data/lib/przelewy24_payment/errors.rb +45 -0
- data/lib/przelewy24_payment/instrumentation.rb +16 -0
- data/lib/przelewy24_payment/money.rb +26 -0
- data/lib/przelewy24_payment/notification.rb +55 -0
- data/lib/przelewy24_payment/payable.rb +20 -0
- data/lib/przelewy24_payment/refund_notification.rb +44 -0
- data/lib/przelewy24_payment/signature.rb +64 -0
- data/lib/przelewy24_payment/test_helpers.rb +20 -0
- data/lib/przelewy24_payment/version.rb +3 -1
- data/lib/przelewy24_payment.rb +26 -187
- metadata +83 -24
- data/.gitignore +0 -1
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/app/helpers/przelewy24_payment/payment_helper.rb +0 -5
- data/app/views/przelewy24_payment/_payment_form.html.haml +0 -28
- data/lib/generators/przelewy24_payment/install/USAGE +0 -8
- data/lib/generators/przelewy24_payment/install/templates/przelewy24_payment.rb +0 -16
- data/lib/przelewy24_payment/przelewy24_payment_controller.rb +0 -67
- data/lib/tasks/test_connection.rake +0 -18
- data/przelewy24_payment.gemspec +0 -19
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
module Api
|
|
5
|
+
class Refunds < Base
|
|
6
|
+
# Refunds part or all of a verified transaction. Amount is in minor units.
|
|
7
|
+
def create(request_id:, order_id:, session_id:, amount:, url_status:, description: nil)
|
|
8
|
+
refunds_uuid = SecureRandom.uuid
|
|
9
|
+
body = {
|
|
10
|
+
requestId: request_id,
|
|
11
|
+
refunds: [ { orderId: order_id, sessionId: session_id, amount: amount, description: description }.compact ],
|
|
12
|
+
refundsUuid: refunds_uuid,
|
|
13
|
+
urlStatus: url_status
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
data = client.post("transaction/refund", body).fetch("data")
|
|
17
|
+
{ "refundsUuid" => refunds_uuid, "refunds" => data }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find(order_id)
|
|
21
|
+
client.get("refund/by/orderId/#{Integer(order_id)}").fetch("data")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
module Api
|
|
5
|
+
class Transactions < Base
|
|
6
|
+
# Registers a transaction and returns its token. Amount is in minor units.
|
|
7
|
+
# Extra keyword arguments are sent as-is in camelCase, e.g. method_ref_id:,
|
|
8
|
+
# method:, additional: { PSU: { IP:, userAgent: } }, transfer_label:,
|
|
9
|
+
# url_card_payment_notification:, time_limit:, wait_for_result:.
|
|
10
|
+
def register(session_id:, amount:, currency:, description:, email:, url_return:, url_status:, **extra)
|
|
11
|
+
body = {
|
|
12
|
+
merchantId: config.merchant_id,
|
|
13
|
+
posId: config.pos_id,
|
|
14
|
+
sessionId: session_id,
|
|
15
|
+
amount: amount,
|
|
16
|
+
currency: currency,
|
|
17
|
+
description: description,
|
|
18
|
+
email: email,
|
|
19
|
+
country: config.default_country,
|
|
20
|
+
language: config.default_language,
|
|
21
|
+
urlReturn: url_return,
|
|
22
|
+
urlStatus: url_status,
|
|
23
|
+
sign: Signature.register(session_id: session_id, merchant_id: config.merchant_id,
|
|
24
|
+
amount: amount, currency: currency, crc: config.crc_key)
|
|
25
|
+
}.merge(extra.transform_keys { |key| key.to_s.camelize(:lower).to_sym })
|
|
26
|
+
|
|
27
|
+
client.post("transaction/register", body).fetch("data").fetch("token")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Confirms a transaction after a status notification. Required for settlement.
|
|
31
|
+
def verify(session_id:, order_id:, amount:, currency:)
|
|
32
|
+
body = {
|
|
33
|
+
merchantId: config.merchant_id,
|
|
34
|
+
posId: config.pos_id,
|
|
35
|
+
sessionId: session_id,
|
|
36
|
+
amount: amount,
|
|
37
|
+
currency: currency,
|
|
38
|
+
orderId: order_id,
|
|
39
|
+
sign: Signature.verify(session_id: session_id, order_id: order_id, amount: amount,
|
|
40
|
+
currency: currency, crc: config.crc_key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
client.put("transaction/verify", body).dig("data", "status") == "success"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_by_session_id(session_id)
|
|
47
|
+
client.get("transaction/by/sessionId/#{ERB::Util.url_encode(session_id)}").fetch("data")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module Przelewy24Payment
|
|
6
|
+
class Client
|
|
7
|
+
attr_reader :configuration
|
|
8
|
+
|
|
9
|
+
def initialize(configuration = Przelewy24Payment.configuration)
|
|
10
|
+
@configuration = configuration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_access
|
|
14
|
+
get("testAccess").dig("data") == true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def transactions = @transactions ||= Api::Transactions.new(self)
|
|
18
|
+
def refunds = @refunds ||= Api::Refunds.new(self)
|
|
19
|
+
def payment_methods = @payment_methods ||= Api::PaymentMethods.new(self)
|
|
20
|
+
def blik = @blik ||= Api::Blik.new(self)
|
|
21
|
+
def cards = @cards ||= Api::Cards.new(self)
|
|
22
|
+
|
|
23
|
+
def get(path, params = nil) = request(:get, path, params)
|
|
24
|
+
def post(path, body) = request(:post, path, body)
|
|
25
|
+
def put(path, body) = request(:put, path, body)
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def request(verb, path, payload)
|
|
30
|
+
configuration.validate!
|
|
31
|
+
response = Instrumentation.instrument("request", verb: verb, path: path) do
|
|
32
|
+
connection.public_send(verb, path, payload)
|
|
33
|
+
end
|
|
34
|
+
handle(response)
|
|
35
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::SSLError => e
|
|
36
|
+
raise ConnectionError, "#{e.class}: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def handle(response)
|
|
40
|
+
return response.body || {} if response.success?
|
|
41
|
+
|
|
42
|
+
body = response.body
|
|
43
|
+
message = body.is_a?(Hash) ? body["error"] : nil
|
|
44
|
+
message = message["message"] if message.is_a?(Hash)
|
|
45
|
+
error_class = case response.status
|
|
46
|
+
when 400, 422 then ValidationError
|
|
47
|
+
when 401, 403 then AuthenticationError
|
|
48
|
+
when 404 then NotFoundError
|
|
49
|
+
when 409 then ConflictError
|
|
50
|
+
when 500.. then ServerError
|
|
51
|
+
else ApiError
|
|
52
|
+
end
|
|
53
|
+
raise error_class.new(message ? "Przelewy24: #{message}" : nil, status: response.status, body: body)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def connection
|
|
57
|
+
@connection ||= Faraday.new(url: "#{configuration.api_url}/") do |conn|
|
|
58
|
+
conn.request :authorization, :basic, configuration.pos_id.to_s, configuration.api_key
|
|
59
|
+
conn.request :json
|
|
60
|
+
conn.response :json, content_type: /\bjson/
|
|
61
|
+
conn.options.open_timeout = configuration.open_timeout
|
|
62
|
+
conn.options.timeout = configuration.timeout
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
class Configuration
|
|
5
|
+
HOSTS = {
|
|
6
|
+
sandbox: "https://sandbox.przelewy24.pl",
|
|
7
|
+
production: "https://secure.przelewy24.pl"
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
ENV_KEYS = {
|
|
11
|
+
merchant_id: "PRZELEWY24_MERCHANT_ID",
|
|
12
|
+
pos_id: "PRZELEWY24_POS_ID",
|
|
13
|
+
api_key: "PRZELEWY24_API_KEY",
|
|
14
|
+
crc_key: "PRZELEWY24_CRC_KEY"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_writer :merchant_id, :pos_id, :api_key, :crc_key
|
|
18
|
+
attr_accessor :default_country, :default_language, :url_options,
|
|
19
|
+
:open_timeout, :timeout, :logger, :allowed_ips,
|
|
20
|
+
:on_payment_verified, :on_payment_failed, :on_refund,
|
|
21
|
+
:max_failed_attempts, :retry_interval, :parent_controller
|
|
22
|
+
attr_reader :environment
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
self.environment = ENV.fetch("PRZELEWY24_ENVIRONMENT", "sandbox")
|
|
26
|
+
@default_country = "PL"
|
|
27
|
+
@default_language = "pl"
|
|
28
|
+
@url_options = {}
|
|
29
|
+
@open_timeout = 5
|
|
30
|
+
@timeout = 15
|
|
31
|
+
@allowed_ips = nil
|
|
32
|
+
@parent_controller = "ActionController::API"
|
|
33
|
+
@max_failed_attempts = 3
|
|
34
|
+
@retry_interval = 1.day
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def environment=(value)
|
|
38
|
+
value = value.to_s.to_sym
|
|
39
|
+
unless HOSTS.key?(value)
|
|
40
|
+
raise ConfigurationError, "environment must be one of #{HOSTS.keys.inspect}, got #{value.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@environment = value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sandbox? = environment == :sandbox
|
|
47
|
+
def production? = environment == :production
|
|
48
|
+
|
|
49
|
+
def merchant_id = Integer(fetch!(:merchant_id))
|
|
50
|
+
def pos_id = Integer(value_for(:pos_id).presence || fetch!(:merchant_id))
|
|
51
|
+
def api_key = fetch!(:api_key).to_s
|
|
52
|
+
def crc_key = fetch!(:crc_key).to_s
|
|
53
|
+
|
|
54
|
+
def host = HOSTS.fetch(environment)
|
|
55
|
+
def api_url = "#{host}/api/v1"
|
|
56
|
+
def gateway_url(token) = "#{host}/trnRequest/#{token}"
|
|
57
|
+
|
|
58
|
+
def logger = @logger || (defined?(::Rails) ? ::Rails.logger : nil)
|
|
59
|
+
|
|
60
|
+
# Raises ConfigurationError naming every missing credential at once.
|
|
61
|
+
# Called lazily on first use, so asset precompilation and generators
|
|
62
|
+
# work without credentials.
|
|
63
|
+
def validate!
|
|
64
|
+
missing = ENV_KEYS.keys.reject { |key| key == :pos_id || value_for(key).present? }
|
|
65
|
+
return self if missing.empty?
|
|
66
|
+
|
|
67
|
+
names = missing.map { |key| "#{key} (#{ENV_KEYS.fetch(key)})" }
|
|
68
|
+
raise ConfigurationError, "Przelewy24Payment is missing: #{names.join(', ')}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def fetch!(key)
|
|
74
|
+
value_for(key).presence || validate!.then { value_for(key) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def value_for(key)
|
|
78
|
+
instance_variable_get("@#{key}").presence || ENV[ENV_KEYS.fetch(key)].presence
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Przelewy24Payment
|
|
2
|
-
class Engine < Rails::Engine
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Przelewy24Payment
|
|
6
|
+
|
|
7
|
+
initializer "przelewy24_payment.filter_parameters" do |app|
|
|
8
|
+
app.config.filter_parameters += %i[sign crc crc_key api_key]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
generators do
|
|
12
|
+
require "generators/przelewy24_payment/install/install_generator"
|
|
13
|
+
end
|
|
3
14
|
end
|
|
4
15
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when a required setting is missing or invalid.
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
|
|
9
|
+
# Raised for transport-level failures (timeouts, DNS, TLS).
|
|
10
|
+
class ConnectionError < Error; end
|
|
11
|
+
|
|
12
|
+
# Raised when starting a payment for something that is already paid.
|
|
13
|
+
class AlreadyPaidError < Error; end
|
|
14
|
+
|
|
15
|
+
# Raised when a webhook signature does not match.
|
|
16
|
+
class SignatureError < Error; end
|
|
17
|
+
|
|
18
|
+
# Raised when a webhook payload lacks required fields.
|
|
19
|
+
class InvalidNotificationError < Error; end
|
|
20
|
+
|
|
21
|
+
# Raised when Przelewy24 answers with an error status.
|
|
22
|
+
class ApiError < Error
|
|
23
|
+
attr_reader :status, :body
|
|
24
|
+
|
|
25
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
26
|
+
@status = status
|
|
27
|
+
@body = body
|
|
28
|
+
super(message || "Przelewy24 API error (HTTP #{status})")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class AuthenticationError < ApiError; end
|
|
33
|
+
class ValidationError < ApiError; end
|
|
34
|
+
class NotFoundError < ApiError; end
|
|
35
|
+
class ServerError < ApiError; end
|
|
36
|
+
|
|
37
|
+
class ConflictError < ApiError; end
|
|
38
|
+
|
|
39
|
+
# BLIK chargeByAlias answered 409: the alias is linked to several bank apps.
|
|
40
|
+
class AmbiguousAliasError < ConflictError
|
|
41
|
+
def alternative_keys
|
|
42
|
+
Array(body.is_a?(Hash) ? body.dig("error", "alternativeKeys") : nil)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module Przelewy24Payment
|
|
6
|
+
# Events are published as "<name>.przelewy24", e.g. "request.przelewy24".
|
|
7
|
+
module Instrumentation
|
|
8
|
+
NAMESPACE = "przelewy24"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def instrument(name, payload = {}, &block)
|
|
13
|
+
ActiveSupport::Notifications.instrument("#{name}.#{NAMESPACE}", payload, &block)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Przelewy24Payment
|
|
6
|
+
# Converts between decimal amounts and the integer minor units
|
|
7
|
+
# (grosze, cents) that Przelewy24 expects.
|
|
8
|
+
module Money
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def to_minor(amount)
|
|
12
|
+
case amount
|
|
13
|
+
when Integer then amount * 100
|
|
14
|
+
when BigDecimal then (amount * 100).round(0, :half_up).to_i
|
|
15
|
+
when String, Rational then (BigDecimal(amount.to_s) * 100).round(0, :half_up).to_i
|
|
16
|
+
when Float then (BigDecimal(amount.to_s) * 100).round(0, :half_up).to_i
|
|
17
|
+
else
|
|
18
|
+
raise ArgumentError, "unsupported amount: #{amount.inspect}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def from_minor(minor)
|
|
23
|
+
BigDecimal(Integer(minor)) / 100
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
# A signed transaction status notification sent by Przelewy24 to urlStatus.
|
|
5
|
+
class Notification
|
|
6
|
+
REQUIRED = %w[merchantId posId sessionId amount currency orderId sign].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :attributes
|
|
9
|
+
|
|
10
|
+
def initialize(attributes, configuration: Przelewy24Payment.configuration)
|
|
11
|
+
@attributes = attributes.to_h.transform_keys(&:to_s)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_json(raw, **options)
|
|
16
|
+
new(JSON.parse(raw), **options)
|
|
17
|
+
rescue JSON::ParserError => e
|
|
18
|
+
raise InvalidNotificationError, "malformed JSON: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def session_id = attributes["sessionId"].to_s
|
|
22
|
+
def order_id = Integer(attributes["orderId"])
|
|
23
|
+
def amount = Integer(attributes["amount"])
|
|
24
|
+
def currency = attributes["currency"].to_s
|
|
25
|
+
def method_id = attributes["methodId"]&.then { |value| Integer(value) }
|
|
26
|
+
def statement = attributes["statement"]
|
|
27
|
+
|
|
28
|
+
def complete?
|
|
29
|
+
REQUIRED.all? { |key| attributes[key].to_s != "" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid?
|
|
33
|
+
complete? &&
|
|
34
|
+
Integer(attributes["merchantId"]) == configuration.merchant_id &&
|
|
35
|
+
Signature.secure_compare?(expected_sign, attributes["sign"])
|
|
36
|
+
rescue ArgumentError, TypeError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def verify!
|
|
41
|
+
raise InvalidNotificationError, "missing required fields" unless complete?
|
|
42
|
+
raise SignatureError, "invalid notification signature" unless valid?
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :configuration
|
|
50
|
+
|
|
51
|
+
def expected_sign
|
|
52
|
+
Signature.notification(attributes, crc: configuration.crc_key)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
# Include in any model that can be paid for (Order, Invoice, ...).
|
|
5
|
+
module Payable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
has_many :przelewy24_payments, as: :payable, class_name: "Przelewy24Payment::Payment", dependent: :restrict_with_exception
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def przelewy24_paid?
|
|
13
|
+
przelewy24_payments.verified.exists?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def latest_przelewy24_payment
|
|
17
|
+
przelewy24_payments.order(:id).last
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
# A signed refund status notification. Status 0 means the refund was
|
|
5
|
+
# completed, 1 means it was rejected.
|
|
6
|
+
class RefundNotification
|
|
7
|
+
REQUIRED = %w[orderId sessionId refundsUuid merchantId amount currency status sign].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :attributes
|
|
10
|
+
|
|
11
|
+
def initialize(attributes, configuration: Przelewy24Payment.configuration)
|
|
12
|
+
@attributes = attributes.to_h.transform_keys(&:to_s)
|
|
13
|
+
@configuration = configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def refunds_uuid = attributes["refundsUuid"].to_s
|
|
17
|
+
def session_id = attributes["sessionId"].to_s
|
|
18
|
+
def order_id = Integer(attributes["orderId"])
|
|
19
|
+
def amount = Integer(attributes["amount"])
|
|
20
|
+
def success? = Integer(attributes["status"]).zero?
|
|
21
|
+
|
|
22
|
+
def complete?
|
|
23
|
+
REQUIRED.all? { |key| attributes[key].to_s != "" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def valid?
|
|
27
|
+
complete? &&
|
|
28
|
+
Signature.secure_compare?(Signature.refund_notification(attributes, crc: configuration.crc_key), attributes["sign"])
|
|
29
|
+
rescue ArgumentError, TypeError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def verify!
|
|
34
|
+
raise InvalidNotificationError, "missing required fields" unless complete?
|
|
35
|
+
raise SignatureError, "invalid refund notification signature" unless valid?
|
|
36
|
+
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :configuration
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Przelewy24Payment
|
|
7
|
+
# Przelewy24 signs the compact JSON of an ordered hash with SHA-384.
|
|
8
|
+
# Key order is part of the contract, so every endpoint has its own builder.
|
|
9
|
+
module Signature
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def register(session_id:, merchant_id:, amount:, currency:, crc:)
|
|
13
|
+
sign("sessionId" => session_id.to_s, "merchantId" => Integer(merchant_id),
|
|
14
|
+
"amount" => Integer(amount), "currency" => currency.to_s, "crc" => crc.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def verify(session_id:, order_id:, amount:, currency:, crc:)
|
|
18
|
+
sign("sessionId" => session_id.to_s, "orderId" => Integer(order_id),
|
|
19
|
+
"amount" => Integer(amount), "currency" => currency.to_s, "crc" => crc.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Optional fields are signed only when Przelewy24 sends them, in the
|
|
23
|
+
# order below.
|
|
24
|
+
def notification(attrs, crc:)
|
|
25
|
+
fields = {}
|
|
26
|
+
fields["merchantId"] = Integer(attrs.fetch("merchantId"))
|
|
27
|
+
fields["posId"] = Integer(attrs.fetch("posId"))
|
|
28
|
+
fields["sessionId"] = attrs.fetch("sessionId").to_s
|
|
29
|
+
fields["amount"] = Integer(attrs.fetch("amount"))
|
|
30
|
+
fields["originAmount"] = Integer(attrs["originAmount"]) if present?(attrs["originAmount"])
|
|
31
|
+
fields["currency"] = attrs.fetch("currency").to_s
|
|
32
|
+
fields["orderId"] = Integer(attrs.fetch("orderId"))
|
|
33
|
+
fields["methodId"] = Integer(attrs["methodId"]) if present?(attrs["methodId"])
|
|
34
|
+
fields["statement"] = attrs["statement"].to_s if present?(attrs["statement"])
|
|
35
|
+
fields["crc"] = crc.to_s
|
|
36
|
+
sign(fields)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def refund_notification(attrs, crc:)
|
|
40
|
+
sign("orderId" => Integer(attrs.fetch("orderId")),
|
|
41
|
+
"sessionId" => attrs.fetch("sessionId").to_s,
|
|
42
|
+
"refundsUuid" => attrs.fetch("refundsUuid").to_s,
|
|
43
|
+
"merchantId" => Integer(attrs.fetch("merchantId")),
|
|
44
|
+
"amount" => Integer(attrs.fetch("amount")),
|
|
45
|
+
"currency" => attrs.fetch("currency").to_s,
|
|
46
|
+
"status" => Integer(attrs.fetch("status")),
|
|
47
|
+
"crc" => crc.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sign(fields)
|
|
51
|
+
Digest::SHA384.hexdigest(JSON.generate(fields))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def present?(value)
|
|
55
|
+
!value.nil? && value.to_s != ""
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def secure_compare?(expected, actual)
|
|
59
|
+
return false unless actual.is_a?(String) && actual.bytesize == expected.bytesize
|
|
60
|
+
|
|
61
|
+
OpenSSL.fixed_length_secure_compare(expected, actual)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
# Builds signed webhook payloads for host-app specs.
|
|
5
|
+
# post przelewy24_payment.status_webhooks_path,
|
|
6
|
+
# params: Przelewy24Payment::TestHelpers.status_payload(payment), as: :json
|
|
7
|
+
module TestHelpers
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def status_payload(payment, order_id: 123_456, method_id: 25, configuration: Przelewy24Payment.configuration)
|
|
11
|
+
attrs = {
|
|
12
|
+
"merchantId" => configuration.merchant_id, "posId" => configuration.pos_id,
|
|
13
|
+
"sessionId" => payment.session_id, "amount" => payment.amount,
|
|
14
|
+
"originAmount" => payment.amount, "currency" => payment.currency,
|
|
15
|
+
"orderId" => order_id, "methodId" => method_id, "statement" => "p24-#{order_id}"
|
|
16
|
+
}
|
|
17
|
+
attrs.merge("sign" => Signature.notification(attrs, crc: configuration.crc_key))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|