qiwi-pay 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/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +22 -0
- data/README.md +296 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/qiwi-pay.rb +42 -0
- data/lib/qiwi-pay/api/capture_operation.rb +44 -0
- data/lib/qiwi-pay/api/payment_operation.rb +29 -0
- data/lib/qiwi-pay/api/refund_operation.rb +47 -0
- data/lib/qiwi-pay/api/response.rb +35 -0
- data/lib/qiwi-pay/api/reversal_operation.rb +47 -0
- data/lib/qiwi-pay/api/status_operation.rb +83 -0
- data/lib/qiwi-pay/cheque.rb +103 -0
- data/lib/qiwi-pay/confirmation.rb +138 -0
- data/lib/qiwi-pay/credentials.rb +66 -0
- data/lib/qiwi-pay/messages_for_codes.rb +91 -0
- data/lib/qiwi-pay/payment_operation.rb +108 -0
- data/lib/qiwi-pay/signature.rb +41 -0
- data/lib/qiwi-pay/version.rb +5 -0
- data/lib/qiwi-pay/wpf/auth_operation.rb +31 -0
- data/lib/qiwi-pay/wpf/payment_operation.rb +64 -0
- data/lib/qiwi-pay/wpf/sale_operation.rb +29 -0
- data/qiwi-pay.gemspec +40 -0
- metadata +165 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QiwiPay
|
4
|
+
# Qiwi payment confirmation (callback)
|
5
|
+
#
|
6
|
+
# @note Confirmation parameters list
|
7
|
+
# Параметр Тип данных Описание
|
8
|
+
# txn_id integer Идентификатор транзакции
|
9
|
+
# txn_status integer Статус транзакции
|
10
|
+
# txn_type integer Тип транзакции
|
11
|
+
# txn_date timestamp Дата транзакции в формате ISO8601 с временной зоной
|
12
|
+
# error_code integer Код ошибки работы системы
|
13
|
+
# pan string(19) Номер карты Покупателя в формате 411111XXXXXX1111
|
14
|
+
# amount decimal Сумма списания
|
15
|
+
# currency integer Валюта суммы списания в цифровом формате согласно ISO 4217
|
16
|
+
# auth_code string(6) Код авторизации
|
17
|
+
# eci string(2) Индикатор E-Commerce операции
|
18
|
+
# card_name string(64) Имя Покупателя, как указано на карте (латинские буквы)
|
19
|
+
# card_bank string(64) Банк-эмитент карты
|
20
|
+
# order_id string(256) Уникальный номер заказа в системе ТСП
|
21
|
+
# ip string(15) IP-адрес Покупателя
|
22
|
+
# email string(64) E-mail Покупателя
|
23
|
+
# country string(3) Страна Покупателя в формате 3-х буквенных кодов согласно ISO 3166-1
|
24
|
+
# city string(64) Город местонахождения Покупателя
|
25
|
+
# region string(6) Регион страны формате геокодов согласно ISO 3166-2
|
26
|
+
# address string(64) Адрес местонахождения Покупателя
|
27
|
+
# phone string(15) Контактный телефон Покупателя
|
28
|
+
# cf1 string(256) Поля для ввода произвольной информации, дополняющей данные по операции. Например - описание услуг ТСП.
|
29
|
+
# cf2 string(256) Поля для ввода произвольной информации, дополняющей данные по операции. Например - описание услуг ТСП.
|
30
|
+
# cf3 string(256) Поля для ввода произвольной информации, дополняющей данные по операции. Например - описание услуг ТСП.
|
31
|
+
# cf4 string(256) Поля для ввода произвольной информации, дополняющей данные по операции. Например - описание услуг ТСП.
|
32
|
+
# cf5 string(256) Поля для ввода произвольной информации, дополняющей данные по операции. Например - описание услуг ТСП.
|
33
|
+
# product_name string(256) Описание услуги которую получает Плательщик.
|
34
|
+
# card_token string(40) Токен карты (если функционал токенизации включен для данного сайта)
|
35
|
+
# card_token_expire timestamp Срок истечения токена карты (если функционал токенизации включен для данного сайта)
|
36
|
+
# sign string(64) Контрольная сумма переданных параметров. Контрольная сумма передается в верхнем регистре.
|
37
|
+
#
|
38
|
+
# @note {timestamp} data type is represented by string in format {YYYY-MM-DDThh:mm:ss±hh:mm}
|
39
|
+
class Confirmation
|
40
|
+
include QiwiPay::MessagesForCodes
|
41
|
+
|
42
|
+
# Available confirmation parameters
|
43
|
+
ALLOWED_PARAMS = %i[
|
44
|
+
txn_id txn_status txn_type txn_date error_code pan
|
45
|
+
amount currency auth_code eci card_name card_bank
|
46
|
+
order_id ip email country city region address phone
|
47
|
+
cf1 cf2 cf3 cf4 cf5
|
48
|
+
product_name
|
49
|
+
card_token card_token_expire
|
50
|
+
sign
|
51
|
+
].freeze
|
52
|
+
|
53
|
+
# Parameters of integer type
|
54
|
+
INTEGER_PARAMS = %i[
|
55
|
+
txn_id
|
56
|
+
txn_status
|
57
|
+
txn_type
|
58
|
+
error_code
|
59
|
+
currency
|
60
|
+
]
|
61
|
+
|
62
|
+
# Request params used to calculate signature
|
63
|
+
SIGN_PARAMS = %i[
|
64
|
+
txn_id
|
65
|
+
txn_status
|
66
|
+
txn_type
|
67
|
+
error_code
|
68
|
+
amount
|
69
|
+
currency
|
70
|
+
ip
|
71
|
+
email
|
72
|
+
].freeze
|
73
|
+
|
74
|
+
# IPs allowed to receive confirmation from
|
75
|
+
ALLOWED_IPS = %w[
|
76
|
+
91.232.231.36
|
77
|
+
79.142.22.81
|
78
|
+
79.142.28.154
|
79
|
+
195.189.102.81
|
80
|
+
].freeze
|
81
|
+
|
82
|
+
attr_reader(*ALLOWED_PARAMS)
|
83
|
+
attr_reader :secret
|
84
|
+
|
85
|
+
# @param crds [Credentials] Api access credentials object
|
86
|
+
# @param params [Hash] Request params
|
87
|
+
def initialize(crds, params)
|
88
|
+
ALLOWED_PARAMS.each do |pname|
|
89
|
+
pval = params.fetch(pname, nil) || params.fetch(pname.to_s, nil)
|
90
|
+
pval = pval.to_i if INTEGER_PARAMS.include?(pname)
|
91
|
+
instance_variable_set "@#{pname}", pval
|
92
|
+
end
|
93
|
+
@secret = crds.secret
|
94
|
+
end
|
95
|
+
|
96
|
+
# Check server IP address validity
|
97
|
+
# @param ip [String]
|
98
|
+
# @return [Boolean]
|
99
|
+
def valid_server_ip?(ip)
|
100
|
+
ALLOWED_IPS.include? ip
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check confirmation params signature validity
|
104
|
+
# @return [Boolean]
|
105
|
+
def valid_sign?
|
106
|
+
calculated_sign.upcase == sign
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check if payment operation was successful (has valid sign and no errors)
|
110
|
+
def success?
|
111
|
+
valid_sign? && !error?
|
112
|
+
end
|
113
|
+
|
114
|
+
# Check if error code present in response
|
115
|
+
def error?
|
116
|
+
!error_code.zero?
|
117
|
+
end
|
118
|
+
|
119
|
+
# Converts confirmation data to hash
|
120
|
+
def to_h
|
121
|
+
{}.tap do |h|
|
122
|
+
ALLOWED_PARAMS.each { |p| h[p] = send(p) }
|
123
|
+
h[:txn_status_message] = txn_status_message
|
124
|
+
h[:txn_type_message] = txn_type_message
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# Calculates signature for parameters
|
131
|
+
def calculated_sign
|
132
|
+
params = SIGN_PARAMS.each_with_object({}) do |p, h|
|
133
|
+
h[p] = send(p).tap { |v| v ? v.to_s : nil }
|
134
|
+
end
|
135
|
+
Signature.new(params, @secret).sign.upcase
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module QiwiPay
|
6
|
+
# QiwiPay access credentials
|
7
|
+
class Credentials
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :secret
|
10
|
+
|
11
|
+
# @return [OpenSSL::X509::Certificate]
|
12
|
+
attr_reader :certificate
|
13
|
+
|
14
|
+
# @return [OpenSSL::PKey::RSA]
|
15
|
+
attr_reader :key
|
16
|
+
|
17
|
+
# @param secret [String] Secret for signature calculation
|
18
|
+
# @param cert [String;OpenSSL::X509::Certificate] Certificate for API auth
|
19
|
+
# @param key [String;OpenSSL::PKey::RSA] Private key for certificate for API auth
|
20
|
+
# @param key_pass [String] Private key passphrase
|
21
|
+
# @param p12 [String;OpenSSL::PKCS12] Container with key and certificate
|
22
|
+
def initialize(secret:,
|
23
|
+
cert: nil,
|
24
|
+
key: nil,
|
25
|
+
key_pass: nil,
|
26
|
+
p12: nil)
|
27
|
+
@secret = secret
|
28
|
+
if p12
|
29
|
+
@certificate, @key = load_p12(p12)
|
30
|
+
else
|
31
|
+
@certificate = create_cert(cert) if cert
|
32
|
+
@key = create_key(key, key_pass) if key
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def create_cert(cert)
|
39
|
+
case cert
|
40
|
+
when OpenSSL::X509::Certificate
|
41
|
+
cert
|
42
|
+
else
|
43
|
+
OpenSSL::X509::Certificate.new(File.read(cert.to_s))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_key(key, key_pass = nil)
|
48
|
+
case key
|
49
|
+
when OpenSSL::PKey::RSA
|
50
|
+
key
|
51
|
+
else
|
52
|
+
OpenSSL::PKey::RSA.new(File.read(key.to_s), key_pass)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_p12(p12_cont)
|
57
|
+
p12 = case p12_cont
|
58
|
+
when OpenSSL::PKCS12
|
59
|
+
p12_cont
|
60
|
+
else
|
61
|
+
OpenSSL::PKCS12.new(File.read(p12_cont.to_s))
|
62
|
+
end
|
63
|
+
[p12.certificate, p12.key]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module QiwiPay
|
4
|
+
# Модуль содержит константы и методы для получения расшифровок кодов
|
5
|
+
module MessagesForCodes
|
6
|
+
# Transaction status messages for codes
|
7
|
+
# @see https://developer.qiwi.com/ru/qiwipay/index.html?json#txn_status
|
8
|
+
TXN_STATUS_MESSAGES = {
|
9
|
+
0 => 'Init',
|
10
|
+
1 => 'Declined',
|
11
|
+
2 => 'Authorized',
|
12
|
+
3 => 'Completed',
|
13
|
+
4 => 'Reconciled',
|
14
|
+
5 => 'Settled'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
# Transaction type messages for codes
|
18
|
+
# @see https://developer.qiwi.com/ru/qiwipay/index.html?json#txn_type
|
19
|
+
TXN_TYPE_MESSAGES = {
|
20
|
+
1 => 'Single-step purchase',
|
21
|
+
2 => 'Purchase: auth',
|
22
|
+
6 => 'Single-step purchase: recurring init',
|
23
|
+
7 => 'Purchase: recurring auth',
|
24
|
+
4 => 'Reversal',
|
25
|
+
3 => 'Refund',
|
26
|
+
5 => 'Recurring',
|
27
|
+
8 => 'Payout',
|
28
|
+
0 => 'Unknown'
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
# Error messages for codes
|
32
|
+
# @see https://developer.qiwi.com/ru/qiwipay/index.html?json#errors
|
33
|
+
ERROR_MESSAGES = {
|
34
|
+
0 => 'No errors',
|
35
|
+
8001 => 'Internal error',
|
36
|
+
8002 => 'Operation not supported',
|
37
|
+
8004 => 'Temporary error',
|
38
|
+
8005 => 'Route not found',
|
39
|
+
8006 => 'Card not supported',
|
40
|
+
8018 => 'Parsing error',
|
41
|
+
8019 => 'Validation error',
|
42
|
+
8020 => 'Amount too big',
|
43
|
+
8021 => 'Merchant site not found',
|
44
|
+
8022 => 'Transaction not found',
|
45
|
+
8023 => 'Transaction expired',
|
46
|
+
8025 => 'Opcode is not allowed',
|
47
|
+
8026 => 'Incorrect parent transaction status',
|
48
|
+
8027 => 'Incorrect parent transaction type',
|
49
|
+
8028 => 'Card expired',
|
50
|
+
8051 => 'Merchant disabled',
|
51
|
+
8052 => 'Incorrect transaction state',
|
52
|
+
8054 => 'Invalid signature',
|
53
|
+
8055 => 'Order already payed',
|
54
|
+
8056 => 'In process',
|
55
|
+
8057 => 'Card locked',
|
56
|
+
8058 => 'Access denied',
|
57
|
+
8059 => 'Currency is not allowed',
|
58
|
+
8060 => 'Amount too big',
|
59
|
+
8061 => 'Currency mismatch',
|
60
|
+
8151 => 'Authentification failed',
|
61
|
+
8152 => 'Transaction rejected by security service',
|
62
|
+
8161 => 'Transaction rejected: try again',
|
63
|
+
8162 => 'Transaction rejected: try again',
|
64
|
+
8163 => 'Transaction rejected: contact QIWI support',
|
65
|
+
8164 => 'Transaction rejected: not enought funds, contact card issuer',
|
66
|
+
8165 => 'Transaction rejected: wrong payment details',
|
67
|
+
8168 => 'Transaction rejected: prohibited, contact card issuer'
|
68
|
+
}.freeze
|
69
|
+
|
70
|
+
# Transaction status description
|
71
|
+
# @return [String]
|
72
|
+
def txn_status_message
|
73
|
+
return unless txn_status
|
74
|
+
TXN_STATUS_MESSAGES[txn_status.to_i] || 'Unknown status'
|
75
|
+
end
|
76
|
+
|
77
|
+
# Transaction type description
|
78
|
+
# @return [String]
|
79
|
+
def txn_type_message
|
80
|
+
return unless txn_type
|
81
|
+
TXN_TYPE_MESSAGES[txn_type.to_i] || 'Unknown type'
|
82
|
+
end
|
83
|
+
|
84
|
+
# Error description for code
|
85
|
+
# @return [String]
|
86
|
+
def error_message
|
87
|
+
return unless error_code
|
88
|
+
ERROR_MESSAGES[error_code.to_i] || 'Unknown error'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module QiwiPay
|
6
|
+
# General patment operation
|
7
|
+
class PaymentOperation
|
8
|
+
# Код операции
|
9
|
+
def self.opcode
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Описание операции
|
14
|
+
def self.description
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
ATTRIBUTES = %i[
|
19
|
+
txn_id
|
20
|
+
merchant_site currency sign amount order_id
|
21
|
+
email country city region address phone
|
22
|
+
cf1 cf2 cf3 cf4 cf5
|
23
|
+
product_name merchant_uid modifiers card_token order_expire
|
24
|
+
callback_url success_url decline_url
|
25
|
+
cheque
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
attr_accessor(*ATTRIBUTES)
|
29
|
+
attr_writer :credentials
|
30
|
+
|
31
|
+
def initialize(credentials, params = {})
|
32
|
+
params.each do |k, v|
|
33
|
+
send("#{k}=", v) if in_params.include?(k.to_sym)
|
34
|
+
end
|
35
|
+
@credentials = credentials
|
36
|
+
end
|
37
|
+
|
38
|
+
def opcode
|
39
|
+
self.class.opcode
|
40
|
+
end
|
41
|
+
|
42
|
+
def description
|
43
|
+
self.class.description
|
44
|
+
end
|
45
|
+
|
46
|
+
# Formatted amount
|
47
|
+
# @return [String]
|
48
|
+
def amount
|
49
|
+
return unless @amount
|
50
|
+
format '%.2f', @amount
|
51
|
+
end
|
52
|
+
|
53
|
+
def callback_url=(url)
|
54
|
+
raise ArgumentError, 'Use https URI as callback_url' unless url.start_with?('https://')
|
55
|
+
@callback_url = url
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param time [String;Time] time to expire order at
|
59
|
+
# Must be a string in format {YYYY-MM-DDThh:mm:ss±hh:mm} or
|
60
|
+
# anything responding to {strftime} message
|
61
|
+
# @example
|
62
|
+
# op.order_expire = Time.now + 3600
|
63
|
+
# op.order_expire = 15.minutes.since
|
64
|
+
def order_expire=(time)
|
65
|
+
@order_expire =
|
66
|
+
if time.respond_to? :strftime
|
67
|
+
time.strftime('%FT%T%:z')
|
68
|
+
else
|
69
|
+
time.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
attr_reader :credentials
|
76
|
+
|
77
|
+
# @return [Array<Symbol>] Operation input parameters
|
78
|
+
def self.in_params
|
79
|
+
raise NotImplementedError
|
80
|
+
end
|
81
|
+
|
82
|
+
def in_params
|
83
|
+
self.class.in_params
|
84
|
+
end
|
85
|
+
|
86
|
+
# Builds hash with meaningful params only
|
87
|
+
# @return [Hash]
|
88
|
+
def params_hash
|
89
|
+
%i[opcode].push(*ATTRIBUTES)
|
90
|
+
.map { |a| [a, send(a).to_s] }
|
91
|
+
.to_h
|
92
|
+
.reject { |_k, v| v.nil? || v.empty? }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Builds and signs request parameters
|
96
|
+
# @return [Hash]
|
97
|
+
def request_params
|
98
|
+
params_hash.tap do |params|
|
99
|
+
params[:sign] = Signature.new(params, credentials.secret).sign
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def cheque
|
104
|
+
return @cheque.encode if @cheque.is_a? Cheque
|
105
|
+
@cheque
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module QiwiPay
|
6
|
+
# Qiwi payment signature calculator
|
7
|
+
class Signature
|
8
|
+
# @param params [Hash] request parameters
|
9
|
+
# @param secret [String] secret key for signature
|
10
|
+
def initialize(params, secret)
|
11
|
+
@params = params.tap do |hs|
|
12
|
+
hs.delete :sign
|
13
|
+
hs.delete 'sign'
|
14
|
+
end
|
15
|
+
@secret = secret.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
# Calculates signature
|
19
|
+
# @return [String] params signature
|
20
|
+
def sign
|
21
|
+
digest = OpenSSL::Digest.new('sha256')
|
22
|
+
OpenSSL::HMAC.hexdigest(digest, @secret, build_params_string)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def build_params_string
|
28
|
+
map_sorted(@params) { |_k, v| v }.reject(&:nil?)
|
29
|
+
.map(&:to_s)
|
30
|
+
.reject(&:empty?)
|
31
|
+
.join('|')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Maps hash yielding key-value pairs ordered by key
|
35
|
+
def map_sorted(hash)
|
36
|
+
hash.keys
|
37
|
+
.sort_by(&:to_sym)
|
38
|
+
.map { |k| yield k, hash[k] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|