epaybg 0.3.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|