iso8583-mkb 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .*.kate-swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in iso8583-mkb.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sergey Gridassov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Iso8583::Mkb
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'iso8583-mkb'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install iso8583-mkb
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/iso8583-mkb/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Sergey Gridasov"]
6
+ gem.email = ["grindars@gmail.com"]
7
+ gem.description = %q{ISO8583 implementation for Credit Bank of Moscow}
8
+ gem.summary = %q{ISO8583 implementation for MKB}
9
+ gem.homepage = "https://github.com/smartkiosk/iso8583-mkb"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "iso8583-mkb"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ISO8583::MKB::VERSION
17
+
18
+ gem.add_dependency 'iso8583'
19
+ gem.add_dependency 'eventmachine'
20
+ end
@@ -0,0 +1,63 @@
1
+ module ISO8583::MKB
2
+ class Authorization < Request
3
+ MTI = "Authorization Request"
4
+ REPEAT_MTI = "Authorization Repeat"
5
+
6
+ MANDATORY = {
7
+ "pan" => "Primary Account Number (PAN)",
8
+ "processing_code" => "Processing Code",
9
+ "amount" => "Amount, Transaction",
10
+ "expiry" => "Date, Expiration",
11
+ "merchant_type" => "Merchant's Type",
12
+ "acquirer_country" => "Acquiring Country Code",
13
+ "entry_mode" => "Point of Service Entry Mode Code",
14
+ "condition_code" => "Point of Service Condition Code",
15
+ "acquirer" => "Acquiring Institution ID code",
16
+ "track2" => "Track 2 data",
17
+ "currency" => "Currency Code, Transaction",
18
+ }
19
+
20
+ OPTIONAL = {
21
+ "conversion_date" => "Date, Conversion",
22
+
23
+ "billing_amount" => "Amount, Cardholder Billing",
24
+ "billing_convrate" => "Conversion, Cardholder Billing",
25
+ "billing_currency" => "Currency Code, Cardholder Billing",
26
+
27
+ "terminal_id" => "Card Acceptor Terminal ID",
28
+ "acceptor_id" => "Card Acceptor ID Code",
29
+ "acceptor_name" => "Card Acceptor Name/Location",
30
+
31
+ "additional" => "Additional Information"
32
+ }
33
+
34
+ RESPONSE = {
35
+ "Authorization Identification Response" => "auth_id",
36
+ "Additional Response Data" => "additional_resp"
37
+ }
38
+
39
+ build_class
40
+
41
+ def reverse
42
+ reversal = Reversal.new
43
+
44
+ [
45
+ :pan, :processing_code, :amount, :expiry, :merchant_type,
46
+ :acquirer_country, :entry_mode, :condition_code, :acquirer,
47
+ :currency
48
+ ].each do |key|
49
+ reversal.send :"#{key}=", instance_variable_get(:"@#{key}")
50
+ end
51
+
52
+ reversal.auth_id = @auth_id
53
+ reversal.response_code = @status
54
+ reversal.original_data = sprintf("%04d%06d%10s%011d%011d",
55
+ @request.request.mti,
56
+ @transaction.trace,
57
+ @request.request["Transmission Date and Time"].strftime("%m%d%H%M%S"),
58
+ @acquirer, @acquirer).to_i
59
+
60
+ reversal
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,108 @@
1
+ module ISO8583::MKB
2
+ module Errors
3
+ ERRORS = {
4
+ "00" => "Approved",
5
+ "01" => "Refer to issuer",
6
+ "02" => "Refer to issuer (special)",
7
+ "03" => "Invalid merchant",
8
+ "04" => "Pick up card",
9
+ "05" => "Do not honor",
10
+ "06" => "Error",
11
+ "07" => "Pick up card (special)",
12
+ "08" => "Honor with identification",
13
+ "09" => "Request in progress",
14
+ "10" => "Approved for partial amount",
15
+ "11" => "VIP Approval",
16
+ "12" => "Invalid transaction",
17
+ "13" => "Invalid amount",
18
+ "14" => "Card number does not exist",
19
+ "15" => "No such issuer",
20
+ "16" => "Approved, update track 3",
21
+ "17" => "Customer cancellation",
22
+ "18" => "Customer dispute",
23
+ "19" => "Re-enter transaction",
24
+ "20" => "Invalid response",
25
+ "21" => "No action taken (no match)",
26
+ "22" => "Suspected malfunction",
27
+ "23" => "Unacceptable transaction fee",
28
+ "24" => "File update not supported by receiver",
29
+ "25" => "Unable to locate record",
30
+ "26" => "Duplicate file update record",
31
+ "27" => "File update field edit error",
32
+ "28" => "File temporarily unavailable",
33
+ "29" => "File update not successful",
34
+ "30" => "Format error",
35
+ "31" => "Issuer sign-off",
36
+ "32" => "Completed partially",
37
+ "33" => "Expired card",
38
+ "34" => "Suspected fraud",
39
+ "35" => "Card acceptor contact acquirer",
40
+ "36" => "Restricted card",
41
+ "37" => "Card acceptor call acquirer",
42
+ "38" => "Allowable PIN tries exceeded",
43
+ "39" => "No credit account",
44
+ "40" => "Function not supported",
45
+ "41" => "Pick up card (lost card)",
46
+ "42" => "No universal account",
47
+ "43" => "Pick up card (stolen card)",
48
+ "44" => "No investment account",
49
+ "51" => "Not sufficient funds",
50
+ "52" => "No checking account",
51
+ "53" => "No savings account",
52
+ "54" => "Expired card",
53
+ "55" => "Incorrect PIN",
54
+ "56" => "No card record",
55
+ "57" => "Transaction not permitted to card",
56
+ "58" => "Transaction not permitted to card",
57
+ "59" => "Suspected fraud",
58
+ "60" => "Card acceptor contact acquirer",
59
+ "61" => "Exceeds withdrawal limit",
60
+ "62" => "Restricted card",
61
+ "63" => "Security violation",
62
+ "64" => "Original amount incorrect",
63
+ "65" => "Activity count exceeded",
64
+ "66" => "Card acceptor call acquirer",
65
+ "67" => "Card pick up at ATM",
66
+ "68" => "Response received too late",
67
+ "75" => "Too many wrong PIN tries",
68
+ "76" => "Previous message not found",
69
+ "77" => "Data does not match original message",
70
+ "80" => "Invalid date",
71
+ "81" => "Cryptographic error in PIN",
72
+ "82" => "Incorrect CVV",
73
+ "83" => "Unable to verify PIN",
74
+ "84" => "Invalid authorization life cycle",
75
+ "85" => "No reason to decline",
76
+ "86" => "PIN validation not possible",
77
+ "88" => "Cryptographic failure",
78
+ "89" => "Authentication failure",
79
+ "90" => "Cutoff is in process",
80
+ "91" => "Issuer or switch inoperative",
81
+ "92" => "No routing path",
82
+ "93" => "Violation of law",
83
+ "94" => "Duplicate transmission",
84
+ "95" => "Reconcile error",
85
+ "96" => "System malfunction",
86
+ "XA" => "Forward to issuer",
87
+ "XD" => "Forward to issuer",
88
+
89
+ # ISO8583::MKB-generated errors
90
+ "C0" => "Request timed out",
91
+ }
92
+
93
+ def self.success?(status)
94
+ # TODO: FIXME
95
+
96
+ status == "00"
97
+ end
98
+
99
+ def self.error(status)
100
+ if ERRORS.include? status
101
+ ERRORS[status]
102
+ else
103
+ "(unknown error #{status})"
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,84 @@
1
+ module ISO8583::MKB
2
+ class Gateway
3
+ def initialize(config)
4
+ uri = URI(config[:dhi])
5
+
6
+ raise "expected tcp scheme for DHI" if uri.scheme != "tcp"
7
+ @dhi_host = uri.host
8
+ @dhi_port = uri.port
9
+ @dhi_open = false
10
+ @reopen_timer = nil
11
+
12
+ @deferred = []
13
+ Logging.logger.info "Gateway started"
14
+ end
15
+
16
+ def run
17
+ EventMachine.run do
18
+ reopen_dhi
19
+ end
20
+ end
21
+
22
+ def stop
23
+ if !@reopen_timer.nil?
24
+ EventMachine.cancel_timer @reopen_timer
25
+ @reopen_timer = nil
26
+ end
27
+
28
+ if !@dhi.nil?
29
+ @dhi_open = false
30
+ @dhi.closed = ->() {}
31
+
32
+ @dhi.close_connection
33
+ @dhi = nil
34
+ end
35
+
36
+ @deferred.clear
37
+ end
38
+
39
+ def new_transaction
40
+ ISO8583Transaction.new self
41
+ end
42
+
43
+ def post_request(request)
44
+
45
+ if @dhi_open ||
46
+ ((request.request.mti == 800 || request.request.mti == 810) && !@dhi.nil?)
47
+ @dhi.request request
48
+ else
49
+ @deferred << request
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def dhi_open
56
+ @dhi_open = true
57
+
58
+ @deferred.each do |request|
59
+ @dhi.request request
60
+ end
61
+
62
+ @deferred.clear
63
+ end
64
+
65
+ def dhi_closed
66
+ @dhi_open = false
67
+ @dhi = nil
68
+
69
+ @reopen_timer = EventMachine.add_timer 15, method(:reopen_dhi)
70
+ end
71
+
72
+ def reopen_dhi
73
+ @reopen_timer = nil
74
+
75
+ Logging.logger.debug "Opening connection to DHI"
76
+
77
+ @dhi = EventMachine.connect @dhi_host, @dhi_port, ISO8583Connection, self
78
+ @dhi.open = method :dhi_open
79
+ @dhi.closed = method :dhi_closed
80
+ @dhi.sign_in
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,225 @@
1
+ module ISO8583::MKB
2
+ class ISO8583Connection < EventMachine::Connection
3
+ attr_reader :gateway
4
+ attr_accessor :open, :closed
5
+
6
+ def initialize(gateway)
7
+ super
8
+
9
+ @gateway = gateway
10
+ @in_air = {}
11
+ @buffer = ""
12
+ @state = :connecting
13
+ @post_sign_on = []
14
+ @ping_timer = nil
15
+ @open = nil
16
+ @closed = nil
17
+ end
18
+
19
+ def post_init
20
+ Logging.logger.debug "Connected to DHI"
21
+ end
22
+
23
+ def sign_in
24
+
25
+ transaction = @gateway.new_transaction
26
+ request = transaction.request do |m|
27
+ m.mti = "Network Management Request"
28
+ m["Network Management Information Code"] = 1
29
+ end
30
+
31
+ @state = :ready
32
+
33
+ request.start(30) do |response|
34
+
35
+ transaction.complete
36
+
37
+ if response.nil?
38
+ Logging.logger.error "No response to Sign On"
39
+
40
+ close_connection_after_writing
41
+ else
42
+ if Errors.success? response.response_code
43
+
44
+ @state = :ready
45
+
46
+ @post_sign_on.each do |request|
47
+ self.request request
48
+ end
49
+
50
+ @post_sign_on.clear
51
+
52
+ send_ping
53
+
54
+ @open.call
55
+ else
56
+ Logging.logger.error "Sign On failed: #{Errors.error response.response_code}"
57
+
58
+ close_connection_after_writing
59
+ end
60
+ end
61
+ end
62
+
63
+ @state = :connecting
64
+ end
65
+
66
+ def receive_data(data)
67
+ @buffer << data
68
+
69
+ until @buffer.empty?
70
+ marker = @buffer.index "\xFF"
71
+
72
+ if marker.nil?
73
+ @buffer.clear
74
+ break
75
+ end
76
+
77
+ if marker != 0
78
+ @buffer.slice! 0, marker
79
+ end
80
+
81
+ break if @buffer.length < 6
82
+
83
+ data_length = @buffer[1..5].to_i
84
+
85
+ break if @buffer.length < 6 + data_length
86
+
87
+ @buffer.slice! 0, 6
88
+ msg = @buffer.slice! 0, data_length
89
+ begin
90
+ receive_message MKBMessage.parse(msg)
91
+ rescue => e
92
+ Logging.logger.error "receive_message failed: #{e}"
93
+ e.backtrace.each { |line| Logging.logger.error line }
94
+ end
95
+ end
96
+ end
97
+
98
+ def unbind
99
+ Logging.logger.debug "Disconnected from DHI"
100
+
101
+ @state = :disconnecting
102
+
103
+ @closed.call
104
+
105
+ EventMachine.cancel_timer @ping_timer unless @ping_timer.nil?
106
+
107
+ @in_air.each do |key, (request, timer)|
108
+ Logging.logger.debug "Request #{request.request.reference} dropped"
109
+
110
+ EventMachine.cancel_timer timer unless timer.nil?
111
+ request.complete nil
112
+ end
113
+
114
+ @post_sign_on.each do |request|
115
+ @gateway.post_request request
116
+ end
117
+
118
+ @in_air.clear
119
+ @post_sign_on.clear
120
+ end
121
+
122
+ def request(request)
123
+ if @state == :connecting
124
+ @post_sign_on << request
125
+ else
126
+
127
+ id = request.request.reference.to_i
128
+
129
+ Logging.logger.debug "Request #{id}: started"
130
+
131
+ raise "duplicate request #{id}" if @in_air.include? id
132
+
133
+ if request.timeout.nil?
134
+ timer = nil
135
+ else
136
+ timer = EventMachine.add_timer(request.timeout) do
137
+ @in_air[id][1] = nil
138
+ forget_request id
139
+ request.complete nil
140
+ end
141
+ end
142
+
143
+ @in_air[id] = [ request, timer ]
144
+
145
+ send_message request.request
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def send_ping
152
+ Logging.logger.debug "Ping"
153
+
154
+ @ping_timer = nil
155
+
156
+ transaction = @gateway.new_transaction
157
+ request = transaction.request do |m|
158
+ m.mti = "Network Management Request"
159
+ m["Network Management Information Code"] = 301
160
+ end
161
+
162
+ request.start(60) do |resp|
163
+ transaction.complete
164
+
165
+ if resp.nil?
166
+ Logging.logger.error "Ping timeout"
167
+
168
+ close_connection_after_writing
169
+ elsif !Errors.success?(resp.response_code)
170
+ Logging.logger.error "Ping failed: #{Errors.error resp.response_code}"
171
+
172
+ close_connection_after_writing
173
+ else
174
+ Logging.logger.debug "Pong"
175
+
176
+ @ping_timer = EventMachine.add_timer(60, method(:send_ping))
177
+ end
178
+ end
179
+ end
180
+
181
+ def forget_request(id)
182
+ Logging.logger.debug "Request #{id}: completed"
183
+
184
+ request, timer = @in_air[id]
185
+ EventMachine.cancel_timer timer unless timer.nil?
186
+
187
+ @in_air.delete id
188
+ end
189
+
190
+ def send_message(message)
191
+ Logging.message message
192
+
193
+ msg = message.to_b
194
+ msg.prepend sprintf("\xFF%05u", msg.length)
195
+
196
+ send_data msg
197
+ end
198
+
199
+ def receive_message(message)
200
+ Logging.message message
201
+
202
+ type = (message.mti / 10) % 10
203
+ case type
204
+ when 0, 2, 4
205
+ Logging.logger.warn "cannot handle type #{type} yet"
206
+
207
+ when 1, 3
208
+ id = message.reference.to_i
209
+
210
+ if !@in_air.include?(id)
211
+ Logging.logger.warn "message #{id} not in air - already timed out?"
212
+ else
213
+ request, = @in_air[id]
214
+
215
+ forget_request id
216
+
217
+ request.complete message
218
+ end
219
+
220
+ else
221
+ Logging.logger.warn "unknown type in MTI: #{type}"
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,39 @@
1
+ module ISO8583
2
+ MMDDCodec = _date_codec("%m%d")
3
+ FhhmmssCodec = _date_codec("%H%M%S")
4
+
5
+ MMDD = Field.new
6
+ MMDD.codec = MMDDCodec
7
+ MMDD.length = 4
8
+
9
+ Fhhmmss = Field.new
10
+ Fhhmmss.codec = FhhmmssCodec
11
+ Fhhmmss.length = 6
12
+
13
+ F63Length = Field.new
14
+ F63Length.name = "F63"
15
+ F63Length.length = 6
16
+ F63Length.codec = ASCII_Number
17
+ F63Length.padding = ->(value) do
18
+ sprintf("\x20\x00\x00%03d", value)
19
+ end
20
+
21
+ F63 = Field.new
22
+ F63.length = F63Length
23
+ F63.codec = ASCII_Number
24
+
25
+ class Message
26
+ def self.parse(str)
27
+ message = self.new
28
+ message.mti, rest = _mti_format.parse(str)
29
+ bmp,rest = Bitmap.parse(rest)
30
+ bmp.each {|bit|
31
+ next if bit < 2
32
+ bmp_def = _definitions[bit]
33
+ value, rest = bmp_def.field.parse(rest)
34
+ message[bit] = value
35
+ }
36
+ message
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ module ISO8583::MKB
2
+ class ISO8583Request
3
+ attr_reader :transaction, :request, :timeout
4
+
5
+ def initialize(transaction, request)
6
+ @transaction = transaction
7
+ @request = request
8
+ @timeout = nil
9
+ @completion = nil
10
+ end
11
+
12
+ def start(timeout = nil, &completion)
13
+ @completion = completion
14
+ @timeout = timeout
15
+
16
+ @transaction.gateway.post_request self
17
+ end
18
+
19
+ def complete(result)
20
+ block = @completion
21
+ @completion = nil
22
+
23
+ block.call result
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ module ISO8583::MKB
2
+ class ISO8583Transaction
3
+ attr_reader :id, :trace, :gateway
4
+
5
+ @@trace_counter = 0
6
+
7
+ def initialize(gateway)
8
+ @gateway = gateway
9
+
10
+ now = Time.now
11
+ @id = (now.year % 100) * 1_00_0000_0000 +
12
+ (now.mday) * 1_0000_0000 +
13
+ now.hour * 3600_000 +
14
+ now.min * 60_000 +
15
+ now.sec * 1_000 +
16
+ now.usec / 1_000
17
+
18
+ @trace = @@trace_counter += 1
19
+ @started_at = now
20
+
21
+ Logging.logger.debug "Transaction #{id} started"
22
+ end
23
+
24
+ def request(&block)
25
+ message = MKBMessage.new
26
+ message["Transmission Date and Time"] = Time.now.utc
27
+ message["Systems Trace Audit Number"] = @trace
28
+ message["Time, Local Transaction"] = @started_at
29
+ message["Date, Local Transaction"] = @started_at
30
+ message.reference = @id.to_s
31
+
32
+ yield message
33
+
34
+ ISO8583Request.new self, message
35
+ end
36
+
37
+ def complete
38
+ # TODO: track transactions
39
+
40
+ Logging.logger.debug "Transaction #{@id} completed"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ module ISO8583::MKB
2
+ module Logging
3
+ class << self
4
+ def started?
5
+ !@logger.nil?
6
+ end
7
+
8
+ def start(logfile = nil)
9
+ logfile = STDERR if logfile.nil?
10
+
11
+ if logfile.kind_of?(Logger)
12
+ @logger = logfile
13
+ else
14
+ @logger = Logger.new logfile, :daily
15
+ end
16
+ end
17
+
18
+ def stop
19
+ @logger = nil
20
+ end
21
+
22
+ def message(message)
23
+ message.to_s.split("\n").each(&@logger.method(:debug))
24
+ end
25
+
26
+ attr_reader :logger
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ module ISO8583::MKB
2
+ class MKBMessage < ISO8583::Message
3
+ include ISO8583
4
+
5
+ mti_format N, length: 4
6
+
7
+ mti 100, "Authorization Request"
8
+ mti 101, "Authorization Repeat"
9
+ mti 102, "ATM Confirmation"
10
+ mti 110, "Authorization Response"
11
+ mti 120, "Advice Authorization Request"
12
+ mti 121, "Advice Repeat"
13
+ mti 130, "Authorization Advice Response"
14
+ mti 200, "Financial Transaction Request" # to host only
15
+ mti 210, "Financial Transaction Response" # from host only
16
+ mti 220, "Financial Transaction Advice" # to host only
17
+ mti 230, "Financial Transaction Response" # from host only
18
+ mti 302, "Interface Exception Request"
19
+ mti 312, "Host Exception Response"
20
+ mti 322, "Host Exception Advice Request"
21
+ mti 332, "Interface Exception Advice Response"
22
+ mti 400, "Reversal Request"
23
+ mti 401, "Reversal Repeat"
24
+ mti 410, "Reversal Response"
25
+ mti 420, "Reversal Advice" # to host only
26
+ mti 430, "Reversal Advice" # from host only
27
+ mti 522, "Issuer Reconciliation Advice" # to host only (for future use)
28
+ mti 532, "Issuer Reconciliation Advice" # from host only
29
+ mti 800, "Network Management Request"
30
+ mti 810, "Network Management Response"
31
+
32
+ bmp 2, "Primary Account Number (PAN)", LLVAR_N, max: 19
33
+ bmp 3, "Processing Code", N, length: 6
34
+ bmp 4, "Amount, Transaction", N, length: 12
35
+ bmp 5, "Amount, Settlement", N, length: 12
36
+ bmp 6, "Amount, Cardholder Billing", N, length: 12
37
+ bmp 7, "Transmission Date and Time", MMDDhhmmss
38
+ bmp 9, "Conversion Rate, Settlement", N, length: 8
39
+ bmp 10, "Conversion, Cardholder Billing", N, length: 8
40
+ bmp 11, "Systems Trace Audit Number", N, length: 6
41
+ bmp 12, "Time, Local Transaction", Fhhmmss
42
+ bmp 13, "Date, Local Transaction", MMDD
43
+ bmp 14, "Date, Expiration", YYMM
44
+ bmp 15, "Date, Settlement", MMDD
45
+ bmp 16, "Date, Conversion", MMDD
46
+ bmp 18, "Merchant's Type", N, length: 4
47
+ bmp 19, "Acquiring Country Code", N, length: 3
48
+ bmp 22, "Point of Service Entry Mode Code", N, length: 4
49
+ bmp 24, "Network International Identifier", AN, length: 3
50
+ bmp 25, "Point of Service Condition Code", N, length: 2
51
+ bmp 28, "Amount, Fee", N, length: 9
52
+ bmp 32, "Acquiring Institution ID code", LLVAR_N, max: 11
53
+ bmp 33, "Forwarding Institution ID code", LLVAR_N, max: 11
54
+ bmp 35, "Track 2 data", LLVAR_B, max: 37
55
+ bmp 37, "Retrieval Reference Number", AN, length: 12
56
+ bmp 38, "Authorization Identification Response", ANP, length: 6
57
+ bmp 39, "Response Code", AN, length: 2
58
+ bmp 41, "Card Acceptor Terminal ID", ANS, length: 8
59
+ bmp 42, "Card Acceptor ID Code", ANS, length: 15
60
+ bmp 43, "Card Acceptor Name/Location", ANS, length: 40
61
+ bmp 44, "Additional Response Data", LLVAR_ANS, max: 25
62
+ bmp 45, "Track 1 data", LLVAR_ANS, max: 80
63
+ bmp 48, "Additional Data-Private", LLVAR_ANS, max: 99
64
+ bmp 49, "Currency Code, Transaction", N, length: 3
65
+ bmp 50, "Currency Code, Settlement", N, length: 3
66
+ bmp 51, "Currency Code, Cardholder Billing", N, length: 3
67
+ bmp 52, "Personal Identification Number Data", B, length: 8
68
+ bmp 53, "Security Related Control Information", N, length: 16
69
+ bmp 54, "Additional Amounts", LLLVAR_ANS, max: 120
70
+ bmp 55, "ICC Related Data", LLLVAR_B, max: 255
71
+ bmp 60, "Additional POS Information", LLVAR_N, max: 6
72
+ bmp 61, "Other Amounts", LLVAR_N, max: 36
73
+ bmp 63, "Message Reason Code", F63, max: 4
74
+ bmp 66, "Settlement Code", A, length: 1
75
+ bmp 70, "Network Management Information Code", N, length: 3
76
+ bmp 73, "Date, Action", N, length: 6
77
+ bmp 74, "Credits, Number", N, length: 10
78
+ bmp 75, "Credits, Reversal Number", N, length: 10
79
+ bmp 76, "Debits, Number", N, length: 10
80
+ bmp 77, "Debits, Reversal Number", N, length: 10
81
+ bmp 86, "Credits, Amount", N, length: 16
82
+ bmp 87, "Credits, Reversal Amount", N, length: 16
83
+ bmp 88, "Debits, Amount", N, length: 16
84
+ bmp 89, "Debits, Reversal Amount", N, length: 16
85
+ bmp 90, "Original Data Elements", N, length: 42
86
+ bmp 91, "File Update Code", AN, length: 1
87
+ bmp 95, "Replacement Amounts", AN, length: 42
88
+ bmp 101, "File Name", LLVAR_AN, max: 17
89
+ bmp 102, "Account From", LLVAR_AN, max: 28
90
+ bmp 103, "Account To", LLVAR_AN, max: 28
91
+ bmp 118, "Intra Country", LLVAR_AN, max: 99
92
+ bmp 120, "Additional Information", LLLVAR_ANS, max: 990
93
+ bmp 122, "Remaining Open-To-Use", LLVAR_ANS, max: 25
94
+
95
+ # TODO: fields 126, 127 (non-ISO encoding)
96
+
97
+ bmp_alias 37, :reference
98
+ bmp_alias 39, :response_code
99
+ end
100
+ end
@@ -0,0 +1,121 @@
1
+ module ISO8583::MKB
2
+ class Request
3
+ class << self
4
+ def build_class
5
+ [ const_get(:MANDATORY), const_get(:OPTIONAL) ].each do |hash|
6
+ hash.each do |key, iso8583_name|
7
+ attr_accessor key.intern
8
+ end
9
+ end
10
+
11
+ const_get(:RESPONSE).each do |iso8583_name, key|
12
+ attr_reader key.intern
13
+ end
14
+ end
15
+ end
16
+
17
+ attr_reader :request, :status
18
+
19
+ attr_accessor :retries
20
+ attr_accessor :timeout
21
+
22
+ def initialize
23
+ @retries = 4
24
+ @timeout = 60
25
+ @retry_with = nil
26
+ @complete = nil
27
+ @request = nil
28
+ end
29
+
30
+ def success?
31
+ Errors.success? @status
32
+ end
33
+
34
+ def status_description
35
+ Errors.error @status
36
+ end
37
+
38
+ def submit(gateway, &complete)
39
+ @transaction = gateway.new_transaction
40
+
41
+ translated_fields = {}
42
+
43
+ self.class.const_get(:MANDATORY).each do |key, iso8583_name|
44
+ value = instance_variable_get :"@#{key}"
45
+ if value.nil?
46
+ raise "required attribute #{key} (#{iso8583_name}) is not specified"
47
+ end
48
+
49
+ translated_fields[iso8583_name] = value
50
+ end
51
+
52
+
53
+ self.class.const_get(:OPTIONAL).each do |key, iso8583_name|
54
+ value = instance_variable_get :"@#{key}"
55
+ unless value.nil?
56
+ translated_fields[iso8583_name] = value
57
+ end
58
+ end
59
+
60
+ @request = @transaction.request do |message|
61
+ message.mti = self.class.const_get(:MTI)
62
+
63
+ translated_fields.each do |key, value|
64
+ message[key] = value
65
+ end
66
+
67
+ message.to_b # to validate encoding of the fields
68
+ end
69
+
70
+ @retry_with = [
71
+ self.class.const_get(:REPEAT_MTI),
72
+ translated_fields
73
+ ]
74
+
75
+ @complete = complete
76
+ @request.start @timeout, &method(:request_completed)
77
+ end
78
+
79
+ private
80
+
81
+ def request_completed(response)
82
+ if response.nil?
83
+ Logging.logger.warn "Request timed out"
84
+
85
+ if @retries == 0
86
+ Logging.logger.warn "Out of retries"
87
+
88
+ complete_request 'C0'
89
+ else
90
+ @retries -= 1
91
+
92
+ Logging.logger.warn "Repeating, #{@retries} more tries"
93
+
94
+ @request = @transaction.request do |message|
95
+ message.mti, fields = @retry_with
96
+
97
+ fields.each do |key, value|
98
+ message[key] = value
99
+ end
100
+
101
+ @request.start @timeout, &method(:request_completed)
102
+ end
103
+ end
104
+ else
105
+ self.class.const_get(:RESPONSE).each do |iso8583_name, key|
106
+ instance_variable_set :"@#{key}", response[iso8583_name]
107
+ end
108
+
109
+ complete_request response.response_code
110
+ end
111
+ end
112
+
113
+ def complete_request(status)
114
+ @status = status
115
+ @transaction.complete
116
+
117
+ complete, @complete = @complete, nil
118
+ complete.call self
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,42 @@
1
+ module ISO8583::MKB
2
+ class Reversal < Request
3
+ MTI = "Reversal Request"
4
+ REPEAT_MTI = "Reversal Repeat"
5
+
6
+ MANDATORY = {
7
+ "pan" => "Primary Account Number (PAN)",
8
+ "processing_code" => "Processing Code",
9
+ "amount" => "Amount, Transaction",
10
+ "expiry" => "Date, Expiration",
11
+ "merchant_type" => "Merchant's Type",
12
+ "acquirer_country" => "Acquiring Country Code",
13
+ "entry_mode" => "Point of Service Entry Mode Code",
14
+ "condition_code" => "Point of Service Condition Code",
15
+ "acquirer" => "Acquiring Institution ID code",
16
+ "currency" => "Currency Code, Transaction",
17
+ "auth_id" => "Authorization Identification Response",
18
+ "response_code" => "Response Code",
19
+ "reason" => "Message Reason Code",
20
+ "original_data" => "Original Data Elements"
21
+ }
22
+
23
+ OPTIONAL = {
24
+ "billing_amount" => "Amount, Cardholder Billing",
25
+ "billing_convrate" => "Conversion, Cardholder Billing",
26
+ "billing_currency" => "Currency Code, Cardholder Billing",
27
+
28
+ "terminal_id" => "Card Acceptor Terminal ID",
29
+ "acceptor_id" => "Card Acceptor ID Code",
30
+ "acceptor_name" => "Card Acceptor Name/Location",
31
+
32
+ "replacement_amount" => "Replacement Amounts",
33
+ "additional" => "Additional Information"
34
+ }
35
+
36
+ RESPONSE = {
37
+
38
+ }
39
+
40
+ build_class
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ module ISO8583::MKB
2
+ class SynchronousGateway
3
+ def initialize(config)
4
+ if EventMachine.reactor_running?
5
+ @loop_is_mine = false
6
+ @gateway = Gateway.new config
7
+ @gateway.run
8
+ else
9
+ @loop_is_mine = true
10
+ @ready = Queue.new
11
+ @thread = Thread.new(config, &method(:thread))
12
+ e = @ready.pop
13
+ @ready = nil
14
+
15
+ raise e unless e.nil?
16
+ end
17
+ end
18
+
19
+ def stop
20
+ EventMachine.schedule do
21
+ @gateway.stop
22
+ EventMachine.stop_event_loop if @loop_is_mine
23
+ end
24
+
25
+ if @loop_is_mine
26
+ @thread.join
27
+ @thread = nil
28
+ end
29
+ end
30
+
31
+ def execute(request)
32
+ queue = Queue.new
33
+
34
+ EventMachine.schedule do
35
+ request.submit(@gateway) { queue.push nil }
36
+ end
37
+
38
+ queue.pop
39
+ end
40
+
41
+ private
42
+
43
+ def thread(config)
44
+ begin
45
+ @gateway = Gateway.new config
46
+
47
+ @ready.push nil
48
+
49
+ EventMachine.epoll
50
+ EventMachine.run do
51
+ EventMachine.error_handler do
52
+ Logging.logger.error "Uncaught exception in EventMachine:"
53
+ Logging.logger.error e.to_s
54
+ e.backtrace.each do |line|
55
+ Logging.logger.error line
56
+ end
57
+ end
58
+
59
+ @gateway.run
60
+ end
61
+
62
+ rescue => e
63
+ Logging.logger.error e.to_s
64
+ e.backtrace.each do |line|
65
+ Logging.logger.error line
66
+ end
67
+
68
+ if !@ready.nil?
69
+ @ready.push e
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ module ISO8583
2
+ module MKB
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ require "logger"
2
+
3
+ require "eventmachine"
4
+ require "iso8583"
5
+
6
+ require "iso8583-mkb/version"
7
+ require "iso8583-mkb/iso8583_patches"
8
+ require "iso8583-mkb/logging"
9
+ require "iso8583-mkb/gateway"
10
+ require "iso8583-mkb/iso8583_connection"
11
+ require "iso8583-mkb/iso8583_transaction"
12
+ require "iso8583-mkb/iso8583_request"
13
+ require "iso8583-mkb/mkb_message"
14
+ require "iso8583-mkb/errors"
15
+ require "iso8583-mkb/request"
16
+ require "iso8583-mkb/authorization"
17
+ require "iso8583-mkb/reversal"
18
+ require "iso8583-mkb/synchronous_gateway"
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iso8583-mkb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sergey Gridasov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: iso8583
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: eventmachine
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: ISO8583 implementation for Credit Bank of Moscow
47
+ email:
48
+ - grindars@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - iso8583-mkb.gemspec
59
+ - lib/iso8583-mkb.rb
60
+ - lib/iso8583-mkb/authorization.rb
61
+ - lib/iso8583-mkb/errors.rb
62
+ - lib/iso8583-mkb/gateway.rb
63
+ - lib/iso8583-mkb/iso8583_connection.rb
64
+ - lib/iso8583-mkb/iso8583_patches.rb
65
+ - lib/iso8583-mkb/iso8583_request.rb
66
+ - lib/iso8583-mkb/iso8583_transaction.rb
67
+ - lib/iso8583-mkb/logging.rb
68
+ - lib/iso8583-mkb/mkb_message.rb
69
+ - lib/iso8583-mkb/request.rb
70
+ - lib/iso8583-mkb/reversal.rb
71
+ - lib/iso8583-mkb/synchronous_gateway.rb
72
+ - lib/iso8583-mkb/version.rb
73
+ homepage: https://github.com/smartkiosk/iso8583-mkb
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.24
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: ISO8583 implementation for MKB
97
+ test_files: []