epaybg 0.3.1 → 1.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 +4 -4
- data/.travis.yml +0 -3
- data/README.md +173 -4
- data/lib/epaybg.rb +1 -0
- data/lib/epaybg/recurring.rb +17 -0
- data/lib/epaybg/recurring/debt/request.rb +17 -0
- data/lib/epaybg/recurring/debt/response.rb +73 -0
- data/lib/epaybg/recurring/payment.rb +37 -0
- data/lib/epaybg/version.rb +1 -1
- data/spec/recurring/recurring_payments_spec.rb +111 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cee407808cf74a63c73afee331e1b67bc6f96ebd
|
4
|
+
data.tar.gz: d02597a63bfb46b455bcd96aba99e0b9aac656f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff4ee0df4a72c92f381e6d8063969c5fd08ccb689e646f515545d8172826def364865affdad77f8c50d6ca22612a055d667f12732f51ac210f90811c08124691
|
7
|
+
data.tar.gz: 88600f123e37752b2a6aab735da4dbd671cf12b5c54612d3d91f009e3b64c181ce07dbd1cfddf7929fa3267361c3aadfd7a79ba51362c63543a9b0eaf316c028
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -21,7 +21,7 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Configuration
|
23
23
|
|
24
|
-
Create the config file config/epaybg.yml with the following contents:
|
24
|
+
Create the config file `config/epaybg.yml` with the following contents:
|
25
25
|
|
26
26
|
```yml
|
27
27
|
production:
|
@@ -37,7 +37,7 @@ test:
|
|
37
37
|
url_idn: "https://demo.epay.bg/ezp/reg_bill.cgi"
|
38
38
|
```
|
39
39
|
|
40
|
-
Set the mode to production in your config/environments/production.rb file:
|
40
|
+
Set the mode to production in your `config/environments/production.rb` file:
|
41
41
|
|
42
42
|
```ruby
|
43
43
|
# Add to bottom of the file
|
@@ -45,10 +45,10 @@ Epaybg.mode = :production
|
|
45
45
|
```
|
46
46
|
|
47
47
|
## Usage
|
48
|
-
Handle a response callback from
|
48
|
+
Handle a response callback from EpayBG:
|
49
49
|
|
50
50
|
```ruby
|
51
|
-
response = Epaybg::Response.new
|
51
|
+
response = Epaybg::Response.new params[:encoded], params[:checksum]
|
52
52
|
response.valid?
|
53
53
|
# => true
|
54
54
|
|
@@ -57,6 +57,7 @@ response.status
|
|
57
57
|
```
|
58
58
|
|
59
59
|
Respond to their callback:
|
60
|
+
|
60
61
|
```ruby
|
61
62
|
response = Epaybg::Response.new(params[:encoded], params[:checksum])
|
62
63
|
|
@@ -67,6 +68,174 @@ response.response_for(:ok)
|
|
67
68
|
=> "INVOICE=f5b1eaf:STATUS=PAID"
|
68
69
|
```
|
69
70
|
|
71
|
+
## Recurring payments
|
72
|
+
|
73
|
+
There is no testing environment. You're on your own.
|
74
|
+
|
75
|
+
Your best bet is to contact EpayBG and ask for the technical specification because it can not be
|
76
|
+
found on their site.
|
77
|
+
|
78
|
+
### Workflow
|
79
|
+
|
80
|
+
If your application has a subscription feature, you may consider implementing recurrent payments.
|
81
|
+
EpayBG has a rather strange way of doing this.
|
82
|
+
|
83
|
+
Here are the steps in their recurring payment cycle.
|
84
|
+
|
85
|
+
- Your user subscribes or makes a purchase.
|
86
|
+
- You provide them with an unique 'subscription' number.
|
87
|
+
- The user registers this number with EpayBG.
|
88
|
+
- From now on, every month EpayBG will 'ask' your application
|
89
|
+
(on a TCP server provided by you, explained below), if the registered
|
90
|
+
subscription has any outstanding debts.
|
91
|
+
- Your application searches for a debt related to the number and returns an answer.
|
92
|
+
- If a debt is returned, the user will receive a notification from EpayBG that there
|
93
|
+
is a pending payment.
|
94
|
+
- The user pays this pending payment.
|
95
|
+
- On the TCP server you will then receive a payment notification, process it and return
|
96
|
+
a response to EpayBG.
|
97
|
+
- The next month you will get another 'debt request' for this number.
|
98
|
+
|
99
|
+
It is rather hard to find this information anywhere in the web. It's unclear even in the official
|
100
|
+
documentation that
|
101
|
+
EpayBG send to the developers.
|
102
|
+
|
103
|
+
### Implementing a TCP server
|
104
|
+
|
105
|
+
In order to accept recurring payment you have to implement a TCP server.
|
106
|
+
|
107
|
+
Here is an example server for accepting incoming TCP requests.
|
108
|
+
|
109
|
+
````ruby
|
110
|
+
require 'socket'
|
111
|
+
|
112
|
+
server = TCPServer.new 2000
|
113
|
+
|
114
|
+
loop do
|
115
|
+
# The code in this block has to be threadsafe
|
116
|
+
Thread.start(server.accept) do |client|
|
117
|
+
begin
|
118
|
+
# This line will read the incoming data stream until the tcp client on the other side sends a
|
119
|
+
# 'shutdown' message.
|
120
|
+
message = client.read
|
121
|
+
|
122
|
+
# Handle the payment.
|
123
|
+
rescue => e
|
124
|
+
# Handle an unexpected error
|
125
|
+
ensure
|
126
|
+
client.close
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
````
|
131
|
+
|
132
|
+
### Handling debt requests
|
133
|
+
|
134
|
+
The example will work with the TCP server implementation described above.
|
135
|
+
EpayBG sends a list of messages separated by new lines `\n`.
|
136
|
+
After that they stop sending data and now only wait for a response in the same TCP session.
|
137
|
+
|
138
|
+
#### An example request
|
139
|
+
|
140
|
+
```
|
141
|
+
XTYPE=QBN
|
142
|
+
AID=700021
|
143
|
+
ACSID=0000900
|
144
|
+
BORIKAID=0000900
|
145
|
+
CLIENTID=67600000000000000
|
146
|
+
LANG=1
|
147
|
+
IDN=000000000001
|
148
|
+
TID=20111010103406700021592704
|
149
|
+
```
|
150
|
+
|
151
|
+
Refer to the technical documentation for further details.
|
152
|
+
|
153
|
+
#### Handling the request
|
154
|
+
|
155
|
+
The gem provides a method which turns the message into a hash.
|
156
|
+
|
157
|
+
````ruby
|
158
|
+
message = client.read
|
159
|
+
data = Epaybg::Recurring.parse_request_body(message)
|
160
|
+
request = Epaybg::Recurring::Debt::Request.new data
|
161
|
+
````
|
162
|
+
|
163
|
+
After you do the processing of the request and find out that this number has a pending payment,
|
164
|
+
you have to build a response object.
|
165
|
+
|
166
|
+
This is done through a Epaybg::Recurring::Debt::Request object. It accepts a hash with parameters,
|
167
|
+
validates them and builds a response array.
|
168
|
+
|
169
|
+
````ruby
|
170
|
+
# ...
|
171
|
+
|
172
|
+
subscription = Subscription.find_by_epay_number request.idn
|
173
|
+
|
174
|
+
response_params = {
|
175
|
+
xvalidto: (Time.now + 5.days), # Due date of the subscription
|
176
|
+
secondid: 45, # Custom id for this debt (Optional)
|
177
|
+
amount: 5000, # Debt in coins (bulgarian stotinki)
|
178
|
+
status: '00', # If the status is not '00' (debt found) the other fields will be ignored.
|
179
|
+
# Look up the documentation for the other status codes.
|
180
|
+
shortdesc: 'Debt description',
|
181
|
+
longdesc: 'Debt details' # Optional
|
182
|
+
}
|
183
|
+
|
184
|
+
response = Epaybg::Recurring::Debt::Response.new response_params
|
185
|
+
````
|
186
|
+
|
187
|
+
Now that you have a response object, you can send back an answer in the current TCP session.
|
188
|
+
|
189
|
+
````ruby
|
190
|
+
response.body_array.each do |element|
|
191
|
+
client.puts element.encode('cp1251') # EpayBG requires that responses are windows-1251 encoded
|
192
|
+
end
|
193
|
+
````
|
194
|
+
|
195
|
+
We have notified EpayBG that this subscription has a pending payment. Now we wait for a payment
|
196
|
+
request.
|
197
|
+
|
198
|
+
### Payments
|
199
|
+
|
200
|
+
After the user pays their debt through EpayBG's system, EpayBG will send a payment notification on
|
201
|
+
the same TCP server used for debt request processing.
|
202
|
+
|
203
|
+
This is an example payment request:
|
204
|
+
|
205
|
+
```
|
206
|
+
XTYPE=QBC
|
207
|
+
AID=700021
|
208
|
+
ACSID=0000900
|
209
|
+
BORIKAID=0000900
|
210
|
+
CLIENTID=67600000000000000
|
211
|
+
IDN=000000000001
|
212
|
+
NEWAMOUNT=000000003000
|
213
|
+
AMOUNT=5000
|
214
|
+
TID=20111010103406700021592705
|
215
|
+
REF=592460592460
|
216
|
+
TDATE=20111010103409
|
217
|
+
```
|
218
|
+
|
219
|
+
Different request types are identified by the `XTYPE` parameter. Refer to the technical
|
220
|
+
documentation for further details.
|
221
|
+
|
222
|
+
````ruby
|
223
|
+
message = client.read
|
224
|
+
data = Epaybg::Recurring.parse_request_body(message)
|
225
|
+
epay_recurrent_payment = Epaybg::Recurring::Payment.new data
|
226
|
+
|
227
|
+
# Handle the payment
|
228
|
+
|
229
|
+
epay_recurrent_payment.respond_with(:ok) # This will generate a response for this session.
|
230
|
+
|
231
|
+
epay_recurrent_payment.response_array.each do |element|
|
232
|
+
client.puts element
|
233
|
+
end
|
234
|
+
````
|
235
|
+
|
236
|
+
The payment is accepted and EpayBG has been notified that the payment has been
|
237
|
+
processed.
|
238
|
+
|
70
239
|
## Contributing
|
71
240
|
|
72
241
|
1. Fork it
|
data/lib/epaybg.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'epaybg/recurring/payment'
|
2
|
+
require 'epaybg/recurring/debt/request'
|
3
|
+
require 'epaybg/recurring/debt/response'
|
4
|
+
|
5
|
+
|
6
|
+
module Epaybg
|
7
|
+
module Recurring
|
8
|
+
def self.parse_request_body(body)
|
9
|
+
array = body.split(/\s/).reject(&:empty?).compact
|
10
|
+
array.inject({}) do |hash, element|
|
11
|
+
key, value = *element.strip.split('=')
|
12
|
+
hash[key.downcase] = value
|
13
|
+
hash
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Epaybg
|
2
|
+
module Recurring
|
3
|
+
module Debt
|
4
|
+
class Request
|
5
|
+
attr_accessor :xtype, :idn, :tid, :aid, :clientid
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
params.each do |k, v|
|
9
|
+
instance_variable_set("@#{k}", v)
|
10
|
+
end
|
11
|
+
|
12
|
+
yield self if block_given?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Epaybg
|
2
|
+
module Recurring
|
3
|
+
module Debt
|
4
|
+
class Response
|
5
|
+
attr_accessor :xvalidto, :secondid, :amount, :status, :shortdesc, :longdesc, :errors
|
6
|
+
|
7
|
+
XTYPE = 'RBN'
|
8
|
+
STATUSES = %w(00 62 14 80 96)
|
9
|
+
|
10
|
+
# Status codes for epay
|
11
|
+
|
12
|
+
# 00 върнато е задължение / debt returned
|
13
|
+
# 62 няма задължение. / no debts for this isd
|
14
|
+
# 14 невалиден номер(idn) / invalid idn
|
15
|
+
# 80 заявката временно не може да бъде изпълнена / timeout, server buisy
|
16
|
+
# 96 обща грешка / other errors
|
17
|
+
|
18
|
+
def initialize(params = {})
|
19
|
+
@errors = []
|
20
|
+
|
21
|
+
params.each do |k, v|
|
22
|
+
instance_variable_set("@#{k}", v)
|
23
|
+
end
|
24
|
+
|
25
|
+
yield self if block_given?
|
26
|
+
validate!
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
[:xvalidto, :amount, :status, :shortdesc].each do |element|
|
31
|
+
@errors << "Attribute #{element} is required!" if send(element).blank?
|
32
|
+
end
|
33
|
+
|
34
|
+
@errors << "'xvalidto' should be a time type field!" unless xvalidto.kind_of?(Time)
|
35
|
+
|
36
|
+
{secondid: 15, shortdesc: 40, longdesc: 1800}.each do |k, v|
|
37
|
+
@errors << "Attribute #{k} is too long. Maximum length should be #{v}." if send(k).to_s.length > v
|
38
|
+
end
|
39
|
+
|
40
|
+
@errors << "Invalid value #{status} for status" unless STATUSES.include?(status)
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?
|
44
|
+
@errors.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def longdesc
|
48
|
+
return nil unless @longdesc
|
49
|
+
|
50
|
+
@longdesc.gsub("\n", "\\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def body_array
|
54
|
+
@body_array = [
|
55
|
+
"XTYPE=#{XTYPE}",
|
56
|
+
"XVALIDTO=#{xvalidto.strftime('%Y%m%d000000')}",
|
57
|
+
"AMOUNT=#{amount}",
|
58
|
+
"STATUS=#{status}",
|
59
|
+
"SHORTDESC=#{shortdesc}"
|
60
|
+
]
|
61
|
+
|
62
|
+
@body_array << "SECONDID=#{secondid}" if secondid
|
63
|
+
@body_array << "LONGDESC=#{longdesc}" if longdesc
|
64
|
+
@body_array
|
65
|
+
end
|
66
|
+
|
67
|
+
def body
|
68
|
+
body_array.join("\n") + "\n"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Epaybg
|
2
|
+
module Recurring
|
3
|
+
class Payment
|
4
|
+
|
5
|
+
RESPONSE_STATUS_CODES = {
|
6
|
+
ok: '00',
|
7
|
+
err: '96',
|
8
|
+
duplicate: '94'
|
9
|
+
}
|
10
|
+
|
11
|
+
attr_accessor :xtype, :idn, :tid, :amount, :secondid, :ref, :aid, :tdate, :clientid
|
12
|
+
|
13
|
+
def initialize(params = {})
|
14
|
+
params.each do |k, v|
|
15
|
+
instance_variable_set("@#{k}", v)
|
16
|
+
end
|
17
|
+
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond_with(symbol)
|
22
|
+
raise 'Invalid symbol' unless RESPONSE_STATUS_CODES.keys.include?(symbol)
|
23
|
+
|
24
|
+
code = RESPONSE_STATUS_CODES[symbol]
|
25
|
+
@response = "XTYPE=RBC\nSTATUS=#{code}\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_array
|
29
|
+
@response.split("\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
def tdate
|
33
|
+
Date.parse(@tdate)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/epaybg/version.rb
CHANGED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
describe 'Recurring Payments' do
|
5
|
+
let(:request_string) {
|
6
|
+
"XTYPE=QBN\nAID=000100\nACSID=0000897\nBORIKAID=0000897\nCLIENTID=67600000000000000\nIDN=6460392\nTID=20160225080306000100705669\n"
|
7
|
+
}
|
8
|
+
|
9
|
+
let(:debt_params){
|
10
|
+
Epaybg::Recurring.parse_request_body(request_string)
|
11
|
+
}
|
12
|
+
|
13
|
+
before do
|
14
|
+
Epaybg.config = YAML.load_file('spec/test_config.yml')
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'Recurring module methods' do
|
18
|
+
it 'should parse a request string' do
|
19
|
+
expect(debt_params).to be_kind_of Hash
|
20
|
+
expect(debt_params['xtype']).to eq 'QBN'
|
21
|
+
expect(debt_params['idn']).to eq '6460392'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'Recurrent payment debt request' do
|
26
|
+
it 'should create a valid object' do
|
27
|
+
request = Epaybg::Recurring::Debt::Request.new debt_params
|
28
|
+
|
29
|
+
expect(request.xtype).not_to be_nil
|
30
|
+
expect(request.idn).not_to be_nil
|
31
|
+
expect(request.tid).not_to be_nil
|
32
|
+
expect(request.aid).not_to be_nil
|
33
|
+
expect(request.clientid).not_to be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'Recurrent payment debt response' do
|
38
|
+
let(:valid_params){
|
39
|
+
{
|
40
|
+
xvalidto: ( Time.now + 5.days ),
|
41
|
+
secondid: 'sid25',
|
42
|
+
amount: 5000,
|
43
|
+
status: '00',
|
44
|
+
shortdesc: 'Some description',
|
45
|
+
longdesc: 'Some long description'
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
it 'create a valid object' do
|
50
|
+
response = Epaybg::Recurring::Debt::Response.new valid_params
|
51
|
+
expect(response).to be_valid
|
52
|
+
expect(response.body_array).not_to be_empty
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'Invalid objects' do
|
56
|
+
it 'should have the right elements' do
|
57
|
+
response = Epaybg::Recurring::Debt::Response.new {}
|
58
|
+
expect(response).not_to be_valid
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should not have too long messages' do
|
62
|
+
invalid_params = {
|
63
|
+
shortdesc: "a" * 60,
|
64
|
+
longdesc: "b" * 1801,
|
65
|
+
}
|
66
|
+
|
67
|
+
response = Epaybg::Recurring::Debt::Response.new(valid_params.merge(invalid_params))
|
68
|
+
expect(response).not_to be_valid
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should not accept invalid status codes' do
|
72
|
+
invalid_params = {
|
73
|
+
status: '1233'
|
74
|
+
}
|
75
|
+
response = Epaybg::Recurring::Debt::Response.new(valid_params.merge(invalid_params))
|
76
|
+
expect(response).not_to be_valid
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe 'Recurring payment object' do
|
83
|
+
let(:valid_params) {
|
84
|
+
{
|
85
|
+
xtype: 'RBN',
|
86
|
+
idn: '0000000001',
|
87
|
+
tid: '423423423423423423',
|
88
|
+
amount: 5000,
|
89
|
+
secondid: 'si45',
|
90
|
+
ref: '234234234234234',
|
91
|
+
aid: '000100',
|
92
|
+
tdate: '20160224173336',
|
93
|
+
clientid: '07728424664'
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
it 'should create a valid object' do
|
98
|
+
payment = Epaybg::Recurring::Payment.new valid_params
|
99
|
+
|
100
|
+
expect(payment.tdate).to be_kind_of(Date)
|
101
|
+
|
102
|
+
expect{ payment.respond_with(:ok) }.not_to raise_error
|
103
|
+
expect{ payment.respond_with(:err) }.not_to raise_error
|
104
|
+
expect{ payment.respond_with(:duplicate) }.not_to raise_error
|
105
|
+
|
106
|
+
expect{ payment.respond_with(:something_else) }.to raise_error('Invalid symbol')
|
107
|
+
|
108
|
+
expect(payment.respond_with(:ok)).not_to be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: epaybg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- gmitrev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -69,12 +69,16 @@ files:
|
|
69
69
|
- epaybg.gemspec
|
70
70
|
- lib/epaybg.rb
|
71
71
|
- lib/epaybg/railtie.rb
|
72
|
+
- lib/epaybg/recurring.rb
|
73
|
+
- lib/epaybg/recurring/debt/request.rb
|
74
|
+
- lib/epaybg/recurring/debt/response.rb
|
75
|
+
- lib/epaybg/recurring/payment.rb
|
72
76
|
- lib/epaybg/response.rb
|
73
77
|
- lib/epaybg/transaction.rb
|
74
78
|
- lib/epaybg/version.rb
|
75
79
|
- lib/epaybg/view_helpers.rb
|
76
|
-
- log/test.log
|
77
80
|
- spec/epaybg/transaction_spec.rb
|
81
|
+
- spec/recurring/recurring_payments_spec.rb
|
78
82
|
- spec/spec_helper.rb
|
79
83
|
- spec/test_config.yml
|
80
84
|
homepage: http://github.com/gmitrev/epaybg
|
@@ -96,12 +100,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
100
|
version: '0'
|
97
101
|
requirements: []
|
98
102
|
rubyforge_project:
|
99
|
-
rubygems_version: 2.
|
103
|
+
rubygems_version: 2.5.1
|
100
104
|
signing_key:
|
101
105
|
specification_version: 4
|
102
106
|
summary: Epaybg provides integration with the epay.bg payment services. It supports
|
103
107
|
payments through epay.bg, credit cards and in EasyPay offices.
|
104
108
|
test_files:
|
105
109
|
- spec/epaybg/transaction_spec.rb
|
110
|
+
- spec/recurring/recurring_payments_spec.rb
|
106
111
|
- spec/spec_helper.rb
|
107
112
|
- spec/test_config.yml
|