pscb_integration 0.9.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 +14 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +209 -0
- data/Rakefile +6 -0
- data/app/controllers/pscb_integration/callback_controller.rb +59 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/routes.rb +3 -0
- data/lib/pscb_integration.rb +29 -0
- data/lib/pscb_integration/api_error.rb +31 -0
- data/lib/pscb_integration/base_api_error.rb +34 -0
- data/lib/pscb_integration/client.rb +140 -0
- data/lib/pscb_integration/config.rb +11 -0
- data/lib/pscb_integration/engine.rb +5 -0
- data/lib/pscb_integration/extended_api_error.rb +81 -0
- data/lib/pscb_integration/version.rb +3 -0
- data/pscb_integration.gemspec +39 -0
- metadata +246 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2b0871fac5b4d141f49b797dd11cab1360ecb81d
|
4
|
+
data.tar.gz: 177540f31376ea3c4aad1e3000ffd62a5b0cef30
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a224d86da1138a74e4f24062071e204b5e54dc062220e9b93eea1a58c8fb03bf86e2e57133e6d439cc494ea195e7e947528a8f9557b4d5d117a89352111f16c
|
7
|
+
data.tar.gz: 4eb961dd75a139fb4a633927c94fe81b858c2018a1f93ab52e3b0bd2f8a472d958cf7d0a263943ab7d52ed4b1fa72289038e5c217eeda63d09b607b60d128fb2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Alex Emelyanov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# PscbIntegration
|
2
|
+
|
3
|
+
PSCB bank payment service integration (trade acquiring). Official documentation http://docs.pscb.ru/oos/, offer https://pscb.ru/corp/services/payment_service/platezhi-v-internete/.
|
4
|
+
|
5
|
+
## Disclamer
|
6
|
+
|
7
|
+
Stay awhile and listen. Are you sure that you need this stuff?
|
8
|
+
|
9
|
+
### Pro:
|
10
|
+
|
11
|
+
* 3% fee for Visa/MasterCard payments
|
12
|
+
|
13
|
+
### Cons:
|
14
|
+
|
15
|
+
* During 1.5 years of our expirience PSCB lost all client's reccurent bingings twice. We lost a lot of revenue because of that.
|
16
|
+
* Very slow personal account web site, it takes from several second to several tens of seconds to load a page.
|
17
|
+
* API looks not solid and have a lack of consistency, it has several ways to return errors in response
|
18
|
+
* PSCB send demo environment payment callbacks to your production server, and if you don't handle them, they send you email like 'We have got invalid response for our HTTP-callbacks', so they can't completely split demo and production environments
|
19
|
+
|
20
|
+
### Your choise
|
21
|
+
|
22
|
+
It's up to you. Probably Yandex.Kassa will be better option, its [API](https://tech.yandex.ru/money/doc/payment-solution/shop-config/intro-docpage/) looks much more solid and reliable (it's Yandex anyway) but it has higher fee 3.5% on [base plan](https://kassa.yandex.ru/fees) for bank cards. But if you revenue more than 1 million RUB per month then only 2.8%. I suppose the choice is obvious here.
|
23
|
+
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
I see you decided to try. Good luck and best wishes.
|
28
|
+
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
gem 'pscb_integration'
|
33
|
+
```
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
|
37
|
+
$ bundle
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Configuration
|
42
|
+
|
43
|
+
Configure it in `<you app folder>/config/initializers/pscb.rb` with:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
PscbIntegration.setup do |config|
|
47
|
+
config.host = 'https://oos.pscb.ru'
|
48
|
+
config.market_place = '<your market place id>'
|
49
|
+
config.secret_key = '<your secret key>'
|
50
|
+
config.demo_secret_key = '<your secret key for demo env>'
|
51
|
+
config.confirm_payment_callback = PaymentService.method(:confirm_pscb_payment_callback)
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
If your application didn't setup `PscbIntegration` configuration with `setup` block then `PscbIntegration::ConfigurationError` will be raised during first attempt to call of any method.
|
56
|
+
|
57
|
+
### Handling payment status notification
|
58
|
+
|
59
|
+
Mount engine in your `routes.rb` file as you wish, e.g.:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
namespace :integration_api do
|
63
|
+
mount PscbIntegration::Engine => '/'
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
Implement callback function assigned to `confirm_pscb_payment_callback`:
|
68
|
+
|
69
|
+
Arguments:
|
70
|
+
`payment` - hash with payment details from PSCB:
|
71
|
+
|
72
|
+
Property | Description
|
73
|
+
-------------------------------------------------------
|
74
|
+
`orderId` | Unique order id generated by merchant
|
75
|
+
`showOrderId` | Not uniqe order id generated by merchant to show it to customer
|
76
|
+
`paymentId` | Order id generated by PSCB
|
77
|
+
`account` | Customer id on merchant side
|
78
|
+
`marketPlace` | Merchant id on PSCB side
|
79
|
+
`paymentMethod` | Payment metho
|
80
|
+
`state` | [Payment state](http://docs.pscb.ru/oos/api.html#api-dopolnitelnyh-vozmozhnostej-merchanta-sostoyaniya-platezha)
|
81
|
+
`stateDate` | Date of last state changing ISO8601
|
82
|
+
`amount` | Order amount
|
83
|
+
`recurrencyToken` | Recurrency token
|
84
|
+
|
85
|
+
`is_demo` - if `true` then payment is from demo environment else from production.
|
86
|
+
|
87
|
+
Callback should return `true` if you system accepts and confirms payment, and `false` (`nil`) in case of rejecting. Example:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
def confirm_pscb_payment_callback(payment, is_demo)
|
91
|
+
if (order = OrderModel.find_by(uid: payment['orderId']))
|
92
|
+
# Some state machine transition
|
93
|
+
order.apply_status(payment['state'])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Build payment url
|
99
|
+
|
100
|
+
[PSCB API documentation](http://docs.pscb.ru/oos/api.html#api-pskb-onlajn-dlya-merchantov-api-platezhnoj-stranicy)
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
client = PscbIntegration::Client.new
|
104
|
+
|
105
|
+
url = client.build_payment_url(
|
106
|
+
nonce: SecureRandom.hex(5), # Salt to avoid replay attack
|
107
|
+
customerAccount: user.id, # Some user id
|
108
|
+
customerRating: 5, # Customer rating
|
109
|
+
customerEmail: user.email, # Customer email
|
110
|
+
customerPhone: user.phone, # Customer phone
|
111
|
+
orderId: '123456', # Unique order id
|
112
|
+
details: 'Some paymnet', # Payment details comment
|
113
|
+
amount: 500, # Amount in RUB
|
114
|
+
paymentMethod: 'ac', # Payment menthod
|
115
|
+
recurrentable: true, # Payment can be repeated by merchant
|
116
|
+
data: {
|
117
|
+
debug: 1, # show debug info in customer browser
|
118
|
+
}
|
119
|
+
)
|
120
|
+
```
|
121
|
+
|
122
|
+
### Pull order status
|
123
|
+
|
124
|
+
[PSCB API documentation](http://docs.pscb.ru/oos/api.html#api-dopolnitelnyh-vozmozhnostej-merchanta-zapros-sostoyaniya-platezha)
|
125
|
+
|
126
|
+
`Client` methods return result in [Either monad](https://github.com/bolshakov/fear#either-documentation) for helping handling errors on different level. Thank you [@bolshakov](https://github.com/bolshakov).
|
127
|
+
|
128
|
+
How it works:
|
129
|
+
|
130
|
+
* `Right` result means success
|
131
|
+
* `Left` result means PSCB returns some conscious error which can require special handling on our side.
|
132
|
+
* any exception means unexpected error (e.g. timeout, network) that we don't know how to handle, and probably best option is to log it and try again later.
|
133
|
+
|
134
|
+
Learn more about [Either monad usage](https://github.com/bolshakov/fear#either-documentation). Example:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
client = PscbIntegration::Client.new
|
138
|
+
|
139
|
+
res = client.pull_order_status(order.id)
|
140
|
+
|
141
|
+
res.reduce(
|
142
|
+
# Left result is handled here
|
143
|
+
# @param error - PscbIntegration::BaseApiError
|
144
|
+
->(error) {
|
145
|
+
# Some special error handling e.g.
|
146
|
+
if error.unknown_payment?
|
147
|
+
# Do something special
|
148
|
+
end
|
149
|
+
},
|
150
|
+
|
151
|
+
# Right result is handled here
|
152
|
+
# @param payment - payment hash from PSCB
|
153
|
+
->(payment) {
|
154
|
+
# Update order status
|
155
|
+
}
|
156
|
+
)
|
157
|
+
```
|
158
|
+
|
159
|
+
### Recurring payment
|
160
|
+
|
161
|
+
[PSCB API documentation](http://docs.pscb.ru/oos/api.html#api-dopolnitelnyh-vozmozhnostej-merchanta-iniciaciya-povtornoj-oplaty)
|
162
|
+
|
163
|
+
Before this call you customer should successefully paid order with `recurrentable` flag. In callback or through status pulling `recurrency_token` will be returned.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
client = PscbIntegration::Client.new
|
167
|
+
|
168
|
+
res = client.recurring_payment(
|
169
|
+
prev_order_uid: prev_order.id, # Previous recurrentable order id
|
170
|
+
new_order_uid: new_order.id, # New order id
|
171
|
+
token: recurrency_token, # Recurrency token from previous order
|
172
|
+
amount: 300, # Amount in RUB
|
173
|
+
)
|
174
|
+
|
175
|
+
res.reduce(
|
176
|
+
->(error) { },
|
177
|
+
->(payment) { },
|
178
|
+
)
|
179
|
+
```
|
180
|
+
|
181
|
+
### Refund order
|
182
|
+
|
183
|
+
[PSCB API documentation](http://docs.pscb.ru/oos/api.html#api-dopolnitelnyh-vozmozhnostej-merchanta-vozvrat-po-platezhu)
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
client = PscbIntegration::Client.new
|
187
|
+
|
188
|
+
res = client.refund_order(order.id)
|
189
|
+
|
190
|
+
res.reduce(
|
191
|
+
->(error) { },
|
192
|
+
->(payment) { },
|
193
|
+
)
|
194
|
+
```
|
195
|
+
|
196
|
+
## Development
|
197
|
+
|
198
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
199
|
+
|
200
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
201
|
+
|
202
|
+
## Contributing
|
203
|
+
|
204
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/holyketzer/pscb_integration.
|
205
|
+
|
206
|
+
|
207
|
+
## License
|
208
|
+
|
209
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module PscbIntegration
|
4
|
+
class CallbackController < ActionController::Base
|
5
|
+
def payment_statuses
|
6
|
+
encrypted_body = request.body.read
|
7
|
+
log("PSCB payment_statuses encrypted params '#{encrypted_body.unpack('H*').first}'")
|
8
|
+
|
9
|
+
body, is_demo_env = *decrypt(encrypted_body)
|
10
|
+
log("PSCB payment_statuses binary params '#{body.unpack('H*').first}' demo_env=#{is_demo_env}")
|
11
|
+
|
12
|
+
json = JSON.parse(body)
|
13
|
+
log("PSCB payment_statuses params #{json.inspect}")
|
14
|
+
|
15
|
+
response = json['payments'].map do |payment|
|
16
|
+
confirmed = config.confirm_payment_callback.call(payment, is_demo_env)
|
17
|
+
|
18
|
+
{
|
19
|
+
orderId: payment['orderId'],
|
20
|
+
action: confirmed ? 'CONFIRM' : 'REJECT'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
log("PSCB payment_statuses response #{response.inspect}")
|
25
|
+
|
26
|
+
render json: { payments: response }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def decrypt(encrypted_body)
|
32
|
+
[
|
33
|
+
client.decrypt(encrypted_body),
|
34
|
+
false,
|
35
|
+
]
|
36
|
+
rescue OpenSSL::Cipher::CipherError
|
37
|
+
log("PSCB payment_statuses decryption with production key is failed, trying with demo key")
|
38
|
+
|
39
|
+
[
|
40
|
+
client.decrypt(encrypted_body, demo: true),
|
41
|
+
true,
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
def log(line)
|
46
|
+
if defined?(Rails)
|
47
|
+
Rails.logger.info(line)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def client
|
52
|
+
@client ||= Client.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def config
|
56
|
+
PscbIntegration.config
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pscb_integration"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/config/routes.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pscb_integration/version'
|
2
|
+
require 'pscb_integration/client'
|
3
|
+
require 'pscb_integration/config'
|
4
|
+
require 'pscb_integration/engine'
|
5
|
+
|
6
|
+
module PscbIntegration
|
7
|
+
ConfigurationError = Class.new(StandardError)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :config
|
11
|
+
|
12
|
+
def config
|
13
|
+
@config ||= begin
|
14
|
+
if @setup_block
|
15
|
+
config = Config.new
|
16
|
+
@setup_block.call(config)
|
17
|
+
config
|
18
|
+
else
|
19
|
+
raise ConfigurationError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Gets called within the initializer
|
25
|
+
def setup(&block)
|
26
|
+
@setup_block = block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PscbIntegration
|
2
|
+
class ApiError < BaseApiError
|
3
|
+
ERROR_CODES = {
|
4
|
+
'NOT_AUTHORIZED' => 'запрос не авторизован',
|
5
|
+
'ILLEGAL_REQUEST' => 'некорректный запрос',
|
6
|
+
'ILLEGAL_ARGUMENTS' => 'передан некорректный набор аргументов',
|
7
|
+
'UNKNOWN_PAYMENT' => 'указанный платёж не обнаружен',
|
8
|
+
'ILLEGAL_ACTION' => 'невозможно совершить требуемое действие',
|
9
|
+
'ILLEGAL_PAYMENT_STATE' => 'невозможно совершить требуемое действие, т.к. платёж находится в неподходящем статусе',
|
10
|
+
'FAILED' => 'невозможно совершить требуемое действие (причина в описании ошибки)',
|
11
|
+
'REPEAT_REQUEST' => 'операция завершена с неопределённым результатом, требуется повторить запрос',
|
12
|
+
'PROCESSING' => 'операция продолжается',
|
13
|
+
'SERVER_ERROR' => 'произошла ошибка на сервере. При возникновении данной ошибки рекомендуется выполнить запрос состояния платежа, чтобы уточнить текущий статус платежа',
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
attr_reader :body
|
17
|
+
|
18
|
+
def initialize(error_code:, body: nil)
|
19
|
+
@error_code = error_code
|
20
|
+
@body = body
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{error_code} #{ERROR_CODES[error_code]} #{body}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def unknown_payment?
|
28
|
+
'UNKNOWN_PAYMENT' == error_code
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PscbIntegration
|
2
|
+
class BaseApiError
|
3
|
+
ERROR_CODES = {
|
4
|
+
timeout: 'timeout error',
|
5
|
+
connection_failed: 'connection failed error',
|
6
|
+
}.freeze
|
7
|
+
|
8
|
+
attr_reader :error_code
|
9
|
+
|
10
|
+
def initialize(error_code)
|
11
|
+
@error_code = error_code
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
ERROR_CODES[error_code]
|
16
|
+
end
|
17
|
+
|
18
|
+
def message
|
19
|
+
to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def timeout?
|
23
|
+
:timeout == error_code
|
24
|
+
end
|
25
|
+
|
26
|
+
def connection_failed?
|
27
|
+
:connection_failed == error_code
|
28
|
+
end
|
29
|
+
|
30
|
+
def unknown_payment?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'faraday'
|
3
|
+
require 'faraday_middleware'
|
4
|
+
require 'fear'
|
5
|
+
require 'pscb_integration/base_api_error'
|
6
|
+
require 'pscb_integration/api_error'
|
7
|
+
require 'pscb_integration/extended_api_error'
|
8
|
+
|
9
|
+
module PscbIntegration
|
10
|
+
class Client
|
11
|
+
include Fear::Either::Mixin
|
12
|
+
|
13
|
+
attr_reader :config
|
14
|
+
|
15
|
+
def initialize(explicit_config = nil)
|
16
|
+
@config = explicit_config || PscbIntegration.config
|
17
|
+
|
18
|
+
@client = Faraday.new(url: config.host) do |faraday|
|
19
|
+
faraday.request :json # form-encode POST params
|
20
|
+
faraday.response :json
|
21
|
+
|
22
|
+
if defined?(Rails)
|
23
|
+
faraday.response :logger, Rails.logger, bodies: true
|
24
|
+
end
|
25
|
+
|
26
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_payment_url(message)
|
31
|
+
json = message.to_json
|
32
|
+
|
33
|
+
params = {
|
34
|
+
marketPlace: config.market_place,
|
35
|
+
message: Base64.urlsafe_encode64(json),
|
36
|
+
signature: signature(json),
|
37
|
+
}
|
38
|
+
|
39
|
+
@client.build_url('pay', params).to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def recurring_payment(prev_order_uid:, new_order_uid:, token:, amount:)
|
43
|
+
body = {
|
44
|
+
orderId: prev_order_uid,
|
45
|
+
newOrderId: new_order_uid,
|
46
|
+
marketPlace: config.market_place,
|
47
|
+
token: token,
|
48
|
+
amount: amount,
|
49
|
+
}.to_json
|
50
|
+
|
51
|
+
handle_response(
|
52
|
+
post('merchantApi/payRecurrent', body)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def pull_order_status(order_uid)
|
57
|
+
body = {
|
58
|
+
orderId: order_uid,
|
59
|
+
marketPlace: config.market_place,
|
60
|
+
}.to_json
|
61
|
+
|
62
|
+
handle_response(
|
63
|
+
post('merchantApi/checkPayment', body)
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def refund_order(order_uid)
|
68
|
+
body = {
|
69
|
+
orderId: order_uid,
|
70
|
+
marketPlace: config.market_place,
|
71
|
+
partialRefund: false,
|
72
|
+
}.to_json
|
73
|
+
|
74
|
+
handle_response(
|
75
|
+
post('merchantApi/refundPayment', body)
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def decrypt(encrypted, demo: false)
|
80
|
+
secret_key = demo ? config.demo_secret_key : config.secret_key
|
81
|
+
|
82
|
+
decipher = OpenSSL::Cipher::AES.new(128, :ECB)
|
83
|
+
decipher.decrypt
|
84
|
+
decipher.key = Digest::MD5.digest(secret_key.to_s)
|
85
|
+
|
86
|
+
plain = decipher.update(encrypted) + decipher.final
|
87
|
+
plain.force_encoding('utf-8')
|
88
|
+
end
|
89
|
+
|
90
|
+
def encrypt(plain)
|
91
|
+
cipher = OpenSSL::Cipher::AES.new(128, :ECB)
|
92
|
+
cipher.encrypt
|
93
|
+
cipher.key = Digest::MD5.digest(config.secret_key.to_s)
|
94
|
+
|
95
|
+
cipher.update(plain) + cipher.final
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def signature(str)
|
101
|
+
Digest::SHA256.new.hexdigest(str + config.secret_key.to_s)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [Either<Faraday::Response, BaseApiError>]
|
105
|
+
def post(path, body)
|
106
|
+
response = @client.post(path) do |request|
|
107
|
+
request.headers['Signature'] = signature(body)
|
108
|
+
request.body = body
|
109
|
+
end
|
110
|
+
|
111
|
+
Right(response.body)
|
112
|
+
rescue Faraday::TimeoutError
|
113
|
+
Left(BaseApiError.new(:timeout))
|
114
|
+
rescue Faraday::Error::ConnectionFailed
|
115
|
+
Left(BaseApiError.new(:connection_failed))
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [Either<Hash, BaseApiError>]
|
119
|
+
def handle_response(response_body)
|
120
|
+
response_body.flat_map do |body|
|
121
|
+
if body && body['status'] == 'STATUS_SUCCESS'
|
122
|
+
Right(body['payment'])
|
123
|
+
elsif body && (error_code = body['errorCode'])
|
124
|
+
Left(ApiError.new(error_code: error_code, body: body))
|
125
|
+
elsif body && (error = body['paymentSystemError'] || body.dig('payment', 'lastError'))
|
126
|
+
Left(
|
127
|
+
ExtendedApiError.new(
|
128
|
+
error_code: error['code'],
|
129
|
+
error_sub_code: error['subCode'],
|
130
|
+
details: error['details'],
|
131
|
+
body: body,
|
132
|
+
)
|
133
|
+
)
|
134
|
+
else
|
135
|
+
Left(ApiError.new(error_code: 'Payment system error', body: body))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module PscbIntegration
|
2
|
+
class ExtendedApiError < ApiError
|
3
|
+
ERROR_CODES = {
|
4
|
+
0 => 'Платеж обработан успешно',
|
5
|
+
1 => 'Платеж находится в обработке',
|
6
|
+
2 => 'Платеж ожидает подтверждения одноразовым паролем',
|
7
|
+
3 => 'Для завершения привязки рекуррентного платежа необходимо передать сумму, заблокированную на карте Клиента',
|
8
|
+
-1 => 'Транзакция отвергнута ПЦ Требуется анализ subCode',
|
9
|
+
-2 => 'Транзакция отвергнута СИСТЕМОЙ Требуется анализ subCode',
|
10
|
+
-3 => 'Неверные параметры платежа, платеж не прошел проверку у поставщика услуги',
|
11
|
+
-4 => 'Карта не привязана: возникает, если карта, с которой пытаются сделать оплату, не привязана к веб-кошельку или услуге, а это требуется, согласно настройке услуги',
|
12
|
+
-5 => 'Неизвестная ошибка, транзакция отвергнута',
|
13
|
+
-14 => 'Не верная SMS подтверждения платежа для Веб-кошелька',
|
14
|
+
-15 => 'Рекуррентные платежи не поддерживаются',
|
15
|
+
-16 => 'Некорректные параметры для рекуррентного платежа',
|
16
|
+
-17 => 'Подпись не верна',
|
17
|
+
-18 => 'Нарушение лимитов СИСТЕМЫ',
|
18
|
+
-19 => 'Попытка фрода',
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
ERROR_SUB_CODES = {
|
22
|
+
100 => 'Сервис недоступен',
|
23
|
+
101 => 'Регламентные работы',
|
24
|
+
102 => 'Недоступен шлюз в МПС',
|
25
|
+
103 => 'Технический сбой при обработке платежа, пользователь пытался задвоить транзакцию (нажал F5 в браузере)',
|
26
|
+
104 => 'Технический сбой при обработке платежа, разрушилась сессия на веб-сервере',
|
27
|
+
105 => 'Не прошла валидация полей',
|
28
|
+
106 => 'Не передан телефон (для услуги, оплачиваемой через веб-кошелек)',
|
29
|
+
-20 => 'Expired transaction',
|
30
|
+
-19 => 'Authentication failed',
|
31
|
+
-17 => 'Access denied',
|
32
|
+
-16 => 'Terminal is locked, please try again',
|
33
|
+
-9 => 'Error in card expiration date field',
|
34
|
+
-4 => 'Server is not responding',
|
35
|
+
-3 => 'No or Invalid response received',
|
36
|
+
-2 => 'Bad CGI request',
|
37
|
+
0 => 'Approved',
|
38
|
+
1 => 'Call your bank',
|
39
|
+
3 => 'Invalid merchant',
|
40
|
+
4 => 'Your card is restricted',
|
41
|
+
5 => 'Transaction declined',
|
42
|
+
6 => 'Error - retry',
|
43
|
+
12 => 'Invalid transaction',
|
44
|
+
13 => 'Invalid amount',
|
45
|
+
14 => 'No such card',
|
46
|
+
15 => 'No such card/issuer',
|
47
|
+
19 => 'Re-enter transaction',
|
48
|
+
20 => 'Invalid response',
|
49
|
+
30 => 'Format error',
|
50
|
+
41 => 'Lost card',
|
51
|
+
43 => 'Stolen card',
|
52
|
+
51 => 'Not sufficient funds',
|
53
|
+
54 => 'Expired card',
|
54
|
+
55 => 'Incorrect PIN',
|
55
|
+
57 => 'Not permitted to client',
|
56
|
+
58 => 'Not permitted to merchant',
|
57
|
+
61 => 'Exceeds amount limit',
|
58
|
+
62 => 'Restricted card',
|
59
|
+
65 => 'Exceeds frequency limit',
|
60
|
+
75 => 'PIN tries exceeded',
|
61
|
+
78 => 'Reserved',
|
62
|
+
82 => 'Time-out at issuer',
|
63
|
+
89 => 'Authentication failure',
|
64
|
+
91 => 'Issuer unavailable',
|
65
|
+
93 => 'Violation of law',
|
66
|
+
96 => 'System malfunction',
|
67
|
+
}.freeze
|
68
|
+
|
69
|
+
attr_reader :error_sub_code, :description
|
70
|
+
|
71
|
+
def initialize(error_code:, error_sub_code:, description:, body:)
|
72
|
+
super(error_code: error_code, body: body)
|
73
|
+
@error_sub_code = error_sub_code
|
74
|
+
@description = description
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"#{description} #{ERROR_CODES[error_code]} #{ERROR_SUB_CODES[error_sub_code]} #{body}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pscb_integration/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pscb_integration"
|
8
|
+
spec.version = PscbIntegration::VERSION
|
9
|
+
spec.authors = ['Alex Emelyanov']
|
10
|
+
spec.email = ['holyketzer@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'PSCB payment gateway integration'
|
13
|
+
spec.description = 'If you not sure, think a little about using of Yandex Kassa instead :)'
|
14
|
+
spec.homepage = 'https://github.com/holyketzer/pscb_integration'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency 'faraday', '~> 0.9.1'
|
26
|
+
spec.add_dependency 'faraday_middleware', '~> 0.9'
|
27
|
+
spec.add_dependency 'fear', '~> 0.5.0'
|
28
|
+
spec.add_dependency 'rails', '~> 4.2'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'addressable', '~> 2.5'
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
32
|
+
spec.add_development_dependency 'fear-rspec', '~> 0.2.0'
|
33
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
34
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
35
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.0'
|
36
|
+
spec.add_development_dependency 'sqlite3'
|
37
|
+
spec.add_development_dependency 'vcr', '~> 2.9'
|
38
|
+
spec.add_development_dependency 'webmock', '~> 1.17'
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pscb_integration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Emelyanov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday_middleware
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fear
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.5.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: addressable
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.5'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.13'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.13'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fear-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.2.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.2.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '10.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '10.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec-rails
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: sqlite3
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: vcr
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '2.9'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '2.9'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: webmock
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '1.17'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '1.17'
|
195
|
+
description: If you not sure, think a little about using of Yandex Kassa instead :)
|
196
|
+
email:
|
197
|
+
- holyketzer@gmail.com
|
198
|
+
executables: []
|
199
|
+
extensions: []
|
200
|
+
extra_rdoc_files: []
|
201
|
+
files:
|
202
|
+
- ".gitignore"
|
203
|
+
- ".rspec"
|
204
|
+
- ".travis.yml"
|
205
|
+
- Gemfile
|
206
|
+
- LICENSE.txt
|
207
|
+
- README.md
|
208
|
+
- Rakefile
|
209
|
+
- app/controllers/pscb_integration/callback_controller.rb
|
210
|
+
- bin/console
|
211
|
+
- bin/setup
|
212
|
+
- config/routes.rb
|
213
|
+
- lib/pscb_integration.rb
|
214
|
+
- lib/pscb_integration/api_error.rb
|
215
|
+
- lib/pscb_integration/base_api_error.rb
|
216
|
+
- lib/pscb_integration/client.rb
|
217
|
+
- lib/pscb_integration/config.rb
|
218
|
+
- lib/pscb_integration/engine.rb
|
219
|
+
- lib/pscb_integration/extended_api_error.rb
|
220
|
+
- lib/pscb_integration/version.rb
|
221
|
+
- pscb_integration.gemspec
|
222
|
+
homepage: https://github.com/holyketzer/pscb_integration
|
223
|
+
licenses:
|
224
|
+
- MIT
|
225
|
+
metadata: {}
|
226
|
+
post_install_message:
|
227
|
+
rdoc_options: []
|
228
|
+
require_paths:
|
229
|
+
- lib
|
230
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
231
|
+
requirements:
|
232
|
+
- - ">="
|
233
|
+
- !ruby/object:Gem::Version
|
234
|
+
version: '0'
|
235
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
236
|
+
requirements:
|
237
|
+
- - ">="
|
238
|
+
- !ruby/object:Gem::Version
|
239
|
+
version: '0'
|
240
|
+
requirements: []
|
241
|
+
rubyforge_project:
|
242
|
+
rubygems_version: 2.6.8
|
243
|
+
signing_key:
|
244
|
+
specification_version: 4
|
245
|
+
summary: PSCB payment gateway integration
|
246
|
+
test_files: []
|