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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QiwiPay
4
+ VERSION = "0.1.0"
5
+ end