dcas-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ # __ Documentation __
2
+ # CreditCard returns are pretty straightforward. They're handled by just this class here, DcasResponse.
3
+ # ACH returns come in two stages, and they have to be handled differently, so they're defined in
4
+ # DcasAchResponse and DcasAchReturn.
5
+ #
6
+ # A DcasResponse object has a status. That status can be one of ['I', 'A', 'G', 'D']
7
+ # These map to a GotoTransaction's statuses, which are as follows:
8
+ # 'R' => Received
9
+ # 'A' => Accepted
10
+ # 'G' => Paid
11
+ # 'D' => Declined
12
+ # 'E' => Processing Error
13
+ # Notice that the three with the stars map directly to three of a DcasResponse object's statuses.
14
+ # The fourth status, 'I', is used to denote an Informational record. These records are never intended
15
+ # to state whether a payment was accepted, declined, or paid, but rather to provide important information
16
+ # back into the system about an account, such as information that needs to be updated soon.
17
+
18
+ module DCAS
19
+ class Response
20
+ class << self
21
+ def responses_in(filename_or_content)
22
+ responses = []
23
+
24
+ if filename_or_content !~ /\n/ && File.exists?(filename_or_content)
25
+ filename_or_content = File.open(filename_or_content, 'rb').map {|l| l.gsub(/[\n\r]+/, "\n")}.join
26
+ end
27
+
28
+ CSV::Reader.parse(filecontents) do |ccrow|
29
+ # Could be simply '9999' -- error!
30
+ begin
31
+ next if ccrow == ['9999']
32
+ # Otherwise, it is in this format:
33
+ # CC,AccountNumber,ReturnCode,ReasonDescription,CustTraceCode
34
+ responses << new(ccrow)
35
+ rescue # Rescue errors caused by the data in the csv.
36
+ end
37
+ end
38
+
39
+ responses
40
+ end
41
+
42
+ # Runs the given block for each response in the given response file.
43
+ def each_response_in(filename_or_content)
44
+ raise ArgumentError, "must include a block!" unless block_given?
45
+ responses_in(filename_or_content).each do |response|
46
+ yield response
47
+ end
48
+ end
49
+ end
50
+
51
+ attr_accessor :account_number, :check_number, :client_id, :status, :information, :description, :ach_submitted
52
+ def attributes
53
+ at = {}
54
+ instance_variables.each do |iv|
55
+ iv.gsub!('@', '')
56
+ at[iv] = instance_variable_get("@#{iv}")
57
+ end
58
+ at
59
+ end
60
+ def attributes=(new_attributes)
61
+ return if new_attributes.nil?
62
+ with(new_attributes.dup) do |a|
63
+ a.stringify_keys!
64
+ a.each {|k,v| send(k + "=", a.delete(k)) if respond_to?("#{k}=")}
65
+ end
66
+ end
67
+
68
+ # Tells if the payment was invalid. By default it's just false, but child classes can redefine this.
69
+ def invalid?
70
+ false
71
+ end
72
+
73
+ CC_RET_CODES = {
74
+ '0' => 'D',
75
+ '1' => 'G',
76
+ '2' => 'I', # I think, we should never get this status. (Haven't yet...)
77
+ '99' => 'E' # These are always server errors
78
+ }
79
+
80
+ def initialize(attrs={})
81
+ new_attrs = {}
82
+ nattrs = attrs.dup
83
+ if nattrs.is_a?(Hash) # Is xml-hash
84
+ nattrs.stringify_keys!
85
+ # status, order_number, transacted_at, transaction_id, description
86
+ new_attrs = nattrs
87
+ elsif nattrs.respond_to?('[]') # Is csv row
88
+ # GotoBilling: MerchantID,FirstName,LastName,CustomerID,Amount,SentDate,SettleDate,TransactionID,TransactionType,Status,Description
89
+ # DCAS: CC,AccountNumber,ReturnCode,ReasonDescription,ConfirmationNumber
90
+ # ret could be 0 (denied), 1 (approved), 2 (call for authorization), or 99 (error)
91
+ new_attrs = {
92
+ :status => (nattrs[2].to_s == 'I' ? 'R' : CC_RET_CODES[nattrs[2].to_s]),
93
+ :description => nattrs[3],
94
+ :account_number => nattrs[1],
95
+ :client_id => nattrs[4][4..-1]
96
+ }
97
+ # This is the case where Malibu must call DCAS for authorization. We haven't come across that need yet, but a note should be made.
98
+ # I'll make it an informational and 'Received' record -- but we have no answer other than this. The transaction won't go through without attention.
99
+ new_attrs[:information] = "MUST CALL FOR Credit Card payment authorization! If you have any questions ask your Manager or the tech guy." if nattrs[2].to_s == 'I'
100
+ end
101
+ self.attributes = new_attrs
102
+ end
103
+
104
+ # def transaction
105
+ # @transaction ||= GotoTransaction.find_by_batch_id_and_client_id(self.batch_id, client_id)
106
+ # end
107
+
108
+ # def record_to_transaction!
109
+ # return unless transaction.status != status && transaction.description != description
110
+ # if transaction.transaction_id.to_i != 0 && transaction.status == 'G' && status == 'D'
111
+ # # Was accepted, now declined.
112
+ # # Delete the transaction if it was previously created.
113
+ # puts "Previously accepted, now declined: delete transaction on master, remove association on goto_transaction"
114
+ # # Helios::Transact.update_on_master(self.transaction.transaction_id, :CType => 1, :client_no => self.transaction_id)
115
+ # transaction.transaction_id = 0
116
+ # end
117
+ # transaction.description = description
118
+ # transaction.status = status
119
+ # transaction.ach_submitted = ach_submitted if ach_submitted
120
+ # transaction.save
121
+ # end
122
+ end
123
+ end
data/lib/dcas.rb ADDED
@@ -0,0 +1,140 @@
1
+ require 'ftools'
2
+
3
+ module DCAS
4
+ TESTING = false
5
+ BUCKET_HOST = 'ftp.ezpaycenters.net'
6
+ OUTGOING_BUCKET = 'outgoing'
7
+ INCOMING_BUCKET = 'incoming'
8
+ STAGING_BUCKET = 'staging'
9
+
10
+ class << self
11
+ # Parses the responses from a response file and returns an array of DCAS::Response objects.
12
+ def parse_response_file(filename_or_content)
13
+ end
14
+ end
15
+
16
+ class Client
17
+ # Instantiate a new Client object which can do authenticated actions in a DCAS FTPS bucket.
18
+ def initialize(options={})
19
+ raise ArgumentError, "must include :username, :password, :company_alias, :company_username, :company_password, and :cache_location" if [:username, :password, :company_alias, :company_username, :company_password, :cache_location].any? {|k| !options.has_key?(k)}
20
+ @username = options[:username]
21
+ @password = options[:password]
22
+ @company_alias = options[:company_alias]
23
+ @company_username = options[:company_username]
24
+ @company_password = options[:company_password]
25
+ @cache_location = options[:cache_location]
26
+ end
27
+
28
+ attr_reader :username, :password, :company_alias, :company_username, :company_password, :cache_location
29
+
30
+ # :nodoc:
31
+ def batches
32
+ @batches ||= []
33
+ end
34
+
35
+ # Begin a new batch associated with this client.
36
+ def new_batch(batch_id)
37
+ batches << DCAS::PaymentBatch.new(self, batch_id)
38
+ batches.last
39
+ end
40
+
41
+ # Uploads a single payments file to the DCAS outgoing payments bucket.
42
+ def submit_payments_file!(filename)
43
+ shortname = filename.gsub(/.*[\\\/][^\\\/]+$/,'')
44
+ with_ftp do |ftp|
45
+ # 1) Create the STAGING folder if it's not already there.
46
+ ftp.mkdir(DCAS::STAGING_BUCKET) unless ftp.nlst.include?(DCAS::STAGING_BUCKET)
47
+ ftp.chdir(DCAS::STAGING_BUCKET)
48
+ # 2) Delete the same filename from the STAGING folder if one exists.
49
+ ftp.delete(shortname) if ftp.nlst.include?(shortname)
50
+ # 3) Upload the file into the STAGING folder.
51
+ ftp.put(filename, shortname)
52
+ # 4) If we're still connected, check the file size of the file, then move it out of STAGING and mark file as completed.
53
+ if ftp.nlst.include?(shortname) && ftp.size(shortname) == File.size(filename)
54
+ ftp.rename(shortname, "../#{DCAS::OUTGOING_BUCKET}/#{shortname}") unless DCAS::TESTING
55
+ else
56
+ raise RuntimeError, "FAILED uploading `#{filename}' - incomplete or unsuccessful upload. Please try again."
57
+ end
58
+ end
59
+ true
60
+ end
61
+
62
+ # Writes all batches to file and submits them to the DCAS outgoing payments bucket.
63
+ def submit_batches!
64
+ File.makedirs(cache_location)
65
+ batches_submitted = 0
66
+ with_ftp do
67
+ # 1) Gather all payments for this client.
68
+ batches.each do |batch| # 2) For each file type (ach, cc) yet to be uploaded:
69
+ filename = cache_location + "/#{company_user}_#{batch.type}_#{Time.now.strftime("%Y%m%d")}.csv"
70
+ # 1) Create the file locally.
71
+ File.open(filename) {|f| f << batch.to_csv }
72
+ # 2) Upload it to the DCAS outgoing payments bucket.
73
+ batches_submitted += 1 if submit_payments_file!(filename)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Checks for response files in the DCAS incoming responses bucket.
79
+ def available_response_files
80
+ with_ftp do |ftp|
81
+ # 3) List the *.csv files in the INCOMING bucket.
82
+ result = if ftp.nlst.include?(DCAS::INCOMING_BUCKET)
83
+ ftp.chdir(DCAS::INCOMING_BUCKET)
84
+ ftp.nlst.select {|f| f =~ /\.csv$/}
85
+ else
86
+ []
87
+ end
88
+ end
89
+ end
90
+
91
+ # Downloads all response files in the DCAS incoming responses bucket.
92
+ def download_response_files!
93
+ files_downloaded = []
94
+ File.makedirs(cache_location + '/returns')
95
+ with_ftp do |ftp|
96
+ files = ftp.list('*.csv')
97
+ files.each do |filels|
98
+ size, file = filels.split(/ +/)[4], filels.split(/ +/)[8..-1].join(' ')
99
+ ftp.get(file, cache_location + '/returns/' + user_suffix + '_' + file)
100
+ files_downloaded << file
101
+ end
102
+ end
103
+ files_downloaded
104
+ end
105
+
106
+
107
+ private
108
+ def user_suffix
109
+ company_username.match(/(?:malibu|maltan|malent)?(.*)(?:VT)?/)[1]
110
+ end
111
+
112
+ def ftp_connection
113
+ @ftp ||= Net::FTPS::Implicit.new(DCAS::BUCKET_HOST, username, password, nil, OpenSSL::SSL::VERIFY_NONE)
114
+ end
115
+ # This allows all functionality to share the same connection, then log out after all work is finished.
116
+ def with_ftp(&block)
117
+ @inside_with_ftp = @inside_with_ftp.to_i + 1
118
+ if block.arity == 1
119
+ yield ftp_connection
120
+ else
121
+ yield
122
+ end
123
+ @inside_with_ftp -= 1
124
+ ftp_done
125
+ end
126
+ def ftp_done
127
+ close_ftp if @inside_with_ftp.to_i == 0
128
+ end
129
+ def close_ftp
130
+ if @ftp
131
+ @ftp.quit
132
+ @ftp.close
133
+ @ftp = nil
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ require 'dcas/payment'
140
+ require 'dcas/response'
@@ -0,0 +1,225 @@
1
+
2
+ # Submitted to ruby-lang.org: http://redmine.ruby-lang.org/issues/show/1371
3
+ # Needs Documentation!
4
+ # quote: "I prefer an approach to modify Net::FTP itself to support implicit
5
+ # (and explicit) FTPS. Please see Net::IMAP in Ruby 1.9."
6
+ # - so I probably should go and edit the net/ftp file to include some of the
7
+ # features of FTPS, when TLS is desired over the FTP connection.
8
+
9
+
10
+ require 'socket'
11
+ require 'openssl'
12
+ require 'net/ftp'
13
+
14
+ class Net::FTPS < Net::FTP
15
+ end
16
+
17
+ class Net::FTPS::Implicit < Net::FTP
18
+ FTP_PORT = 990
19
+
20
+ def initialize(host=nil, user=nil, passwd=nil, acct=nil, verify_mode=OpenSSL::SSL::VERIFY_PEER)
21
+ super(host, user, passwd, acct)
22
+ @passive = true
23
+ @binary = false
24
+ @debug_mode = false
25
+ @data_protection = 'P'
26
+ @data_protected = false
27
+ @verify_mode = verify_mode
28
+ end
29
+ attr_accessor :data_protection
30
+
31
+ def open_socket(host, port, data_socket=false)
32
+ tcpsock = if defined? SOCKSsocket and ENV["SOCKS_SERVER"]
33
+ @passive = true
34
+ SOCKSsocket.open(host, port)
35
+ else
36
+ TCPSocket.new(host, port)
37
+ end
38
+ if !data_socket || @data_protection == 'P'
39
+ ssl_context = OpenSSL::SSL::SSLContext.new('SSLv23')
40
+ ssl_context.verify_mode = @verify_mode
41
+ ssl_context.key = nil
42
+ ssl_context.cert = nil
43
+ ssl_context.timeout = 10
44
+
45
+ sock = OpenSSL::SSL::SSLSocket.new(tcpsock, ssl_context)
46
+ sock.connect
47
+ else
48
+ sock = tcpsock
49
+ end
50
+ return sock
51
+ end
52
+ private :open_socket
53
+
54
+ def connect(host, port=FTP_PORT)
55
+ @sock = open_socket(host, port)
56
+ mon_initialize
57
+ getresp
58
+ at_exit {
59
+ if @sock && !@sock.closed?
60
+ voidcmd("ABOR") rescue EOFError
61
+ voidcmd("QUIT") rescue EOFError
62
+ close
63
+ end
64
+ }
65
+ end
66
+
67
+ def abort
68
+ voidcmd("ABOR") rescue EOFError
69
+ end
70
+
71
+ def quit
72
+ voidcmd("QUIT") rescue EOFError
73
+ end
74
+
75
+ def close
76
+ @sock.close # SSL
77
+ @sock.io.close # TCP
78
+ end
79
+
80
+ def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
81
+ synchronize do
82
+ voidcmd("TYPE I")
83
+ conn = transfercmd(cmd, rest_offset)
84
+ data = get_data(conn,blocksize)
85
+ yield(data)
86
+ voidresp
87
+ end
88
+ end
89
+
90
+ def get_data(sock,blocksize=1024)
91
+ timeout = 10
92
+ starttime = Time.now
93
+ buffer = ''
94
+ timeouts = 0
95
+ catch :done do
96
+ loop do
97
+ event = select([sock],nil,nil,0.5)
98
+ if event.nil? # nil would be a timeout, we'd do nothing and start loop over. Of course here we really have no timeout...
99
+ timeouts += 0.5
100
+ break if timeouts > timeout
101
+ else
102
+ event[0].each do |sock| # Iterate through all sockets that have pending activity
103
+ if sock.eof? # Socket's been closed by the client
104
+ throw :done
105
+ else
106
+ buffer << sock.readpartial(blocksize)
107
+ if block_given? # we're in line-by-line mode
108
+ lines = buffer.split(/\r?\n/)
109
+ buffer = buffer =~ /\n$/ ? '' : lines.pop
110
+ lines.each do |line|
111
+ yield(line)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ sock.close
120
+ buffer
121
+ end
122
+
123
+ def retrlines(cmd) # :yield: line
124
+ synchronize do
125
+ voidcmd("TYPE A")
126
+ voidcmd("STRU F")
127
+ voidcmd("MODE S")
128
+ conn = transfercmd(cmd)
129
+ get_data(conn) do |line|
130
+ yield(line)
131
+ end
132
+ getresp
133
+ end
134
+ end
135
+
136
+ #
137
+ # Puts the connection into binary (image) mode, issues the given server-side
138
+ # command (such as "STOR myfile"), and sends the contents of the file named
139
+ # +file+ to the server. If the optional block is given, it also passes it
140
+ # the data, in chunks of +blocksize+ characters.
141
+ #
142
+ def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
143
+ if rest_offset
144
+ file.seek(rest_offset, IO::SEEK_SET)
145
+ end
146
+ synchronize do
147
+ voidcmd("TYPE I")
148
+ conn = transfercmd(cmd, rest_offset)
149
+ loop do
150
+ buf = file.read(blocksize)
151
+ break if buf == nil
152
+ conn.write(buf)
153
+ yield(buf) if block
154
+ end
155
+ conn.close # closes the SSL
156
+ conn.io.close # closes the TCP below it
157
+ voidresp
158
+ end
159
+ end
160
+
161
+ #
162
+ # Puts the connection into ASCII (text) mode, issues the given server-side
163
+ # command (such as "STOR myfile"), and sends the contents of the file
164
+ # named +file+ to the server, one line at a time. If the optional block is
165
+ # given, it also passes it the lines.
166
+ #
167
+ def storlines(cmd, file, &block) # :yield: line
168
+ synchronize do
169
+ voidcmd("TYPE A")
170
+ conn = transfercmd(cmd)
171
+ loop do
172
+ buf = file.gets
173
+ break if buf == nil
174
+ if buf[-2, 2] != CRLF
175
+ buf = buf.chomp + CRLF
176
+ end
177
+ conn.write(buf)
178
+ yield(buf) if block
179
+ end
180
+ conn.close # closes the SSL
181
+ conn.io.close # closes the TCP below it
182
+ voidresp
183
+ end
184
+ end
185
+
186
+ def transfercmd(cmd, rest_offset=nil)
187
+ unless @data_protected
188
+ voidcmd('PBSZ 0')
189
+ sendcmd("PROT #{@data_protection}")
190
+ @data_protected = true
191
+ end
192
+
193
+ if @passive
194
+ host, port = makepasv
195
+ if @resume and rest_offset
196
+ resp = sendcmd("REST " + rest_offset.to_s)
197
+ if resp[0] != ?3
198
+ raise FTPReplyError, resp
199
+ end
200
+ end
201
+ putline(cmd)
202
+ conn = open_socket(host, port, true)
203
+ resp = getresp # Should be a 150 response
204
+ if resp[0] != ?1
205
+ raise FTPReplyError, resp
206
+ end
207
+ else
208
+ sock = makeport
209
+ if @resume and rest_offset
210
+ resp = sendcmd("REST " + rest_offset.to_s)
211
+ if resp[0] != ?3
212
+ raise FTPReplyError, resp
213
+ end
214
+ end
215
+ resp = sendcmd(cmd)
216
+ if resp[0] != ?1
217
+ raise FTPReplyError, resp
218
+ end
219
+ conn = sock.accept
220
+ sock.close
221
+ end
222
+ return conn
223
+ end
224
+ private :transfercmd
225
+ end
@@ -0,0 +1,11 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe DCAS::Response do
4
+ before :all do
5
+ end
6
+
7
+ it "should parse a CreditCard response correctly"
8
+
9
+ it "should parse an ACH response correctly"
10
+
11
+ end
data/spec/dcas_spec.rb ADDED
@@ -0,0 +1,50 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Dcas - Comprehensive failure frequency test" do
4
+ before :all do
5
+ DCAS::TESTING = true
6
+ @fake_client = DCAS::Client.new(
7
+ :username => 'none',
8
+ :password => 'none',
9
+ :company_alias => 'tester',
10
+ :company_username => 'tester1',
11
+ :company_password => 'fakeness1',
12
+ :cache_location => 'none'
13
+ )
14
+ end
15
+
16
+ it "should generate Ach payment files correctly" do
17
+ ach_batch = @fake_client.new_batch(1)
18
+ Fixtures[:TestPayments][:Ach].collect {|p| ach_batch << DCAS::AchPayment.new(*p) }
19
+ ach_payments_file = ach_batch.to_csv
20
+ ach_payments_file.should eql(File.read('spec/fixtures/ach_payments.csv'))
21
+ end
22
+
23
+ it "should generate CreditCard payment files correctly" do
24
+ cc_batch = @fake_client.new_batch(1)
25
+ Fixtures[:TestPayments][:CreditCard].collect {|p| cc_batch << DCAS::CreditCardPayment.new(*p) }
26
+ cc_payments_file = cc_batch.to_csv
27
+ cc_payments_file.should eql(File.read('spec/fixtures/credit_card_payments.csv'))
28
+ end
29
+
30
+ it "should be able to complete an entire mock procedure without failing" do
31
+ lambda {
32
+ # Depends: Fixture load of a list of DCAS logins to test
33
+ # Depends: Fixed test files
34
+ Fixtures[:Clients].each do |client|
35
+ cc_batch = client.new_batch(1)
36
+ Fixtures[:TestPayments][:CreditCard].each {|p| cc_batch << DCAS::CreditCardPayment.new(*p) }
37
+
38
+ ach_batch = client.new_batch(1)
39
+ Fixtures[:TestPayments][:Ach].each {|p| ach_batch << DCAS::AchPayment.new(*p) }
40
+
41
+ client.submit_batches!.should eql(Fixtures[:PaymentFiles].length)
42
+ end
43
+ Fixtures[:Clients].each do |client|
44
+ client.download_response_files!
45
+ end
46
+ }.should_not raise_error
47
+ end
48
+
49
+ # I can't fake it failing without too much extra work, so I'm just testing successes for now.
50
+ end
File without changes
@@ -0,0 +1,11 @@
1
+ HD,tester,tester1,fakeness1,Check
2
+ CA,124083517,182045828,2010010,18.88,,Dan Hall,,,,,,,,,100111428,,,Debit,,Checking
3
+ CA,524881452,584218548,2010010,18.88,,Karie Moore,,,,,,,,,100111200,,,Debit,,Checking
4
+ CA,107502429,94217885428,2010010,18.88,,Therese Boon,,,,,,,,,100111829,,,Debit,,Checking
5
+ CA,342882012,24254,2010010,18.88,,Gretchen Gutierrez,,,,,,,,,100111282,,,Debit,,Savings
6
+ CA,547107458,280458548254,2010010,18.88,,Audrice Pitto,,,,,,,,,100111795,,,Debit,,Checking
7
+ CA,125480806,641081508245,2010010,18.88,,Rachel Ruppert,,,,,,,,,100111537,,,Debit,,Checking
8
+ CA,982458912,52488542858,2010010,18.88,,Judith Witlock,,,,,,,,,100111892,,,Debit,,Checking
9
+ CA,108458068,4728458248,2010010,18.88,,Kris Martell,,,,,,,,,100111172,,,Debit,,Savings
10
+ CA,645388043,38184599459,2010010,18.88,,Michell Penderson,,,,,,,,,100111284,,,Debit,,Checking
11
+ CA,543882548,543886028,2010010,18.88,,Polly Clark,,,,,,,,,100111756,,,Debit,,Checking
@@ -0,0 +1,81 @@
1
+ ---
2
+ -
3
+ - '00111428'
4
+ - 'Dan Hall'
5
+ - '18.88'
6
+ - 'Checking'
7
+ - '124083517'
8
+ - '182045828'
9
+ - '2010010'
10
+ -
11
+ - '00111200'
12
+ - 'Karie Moore'
13
+ - '18.88'
14
+ - 'Checking'
15
+ - '524881452'
16
+ - '584218548'
17
+ - '2010010'
18
+ -
19
+ - '00111829'
20
+ - 'Therese Boon'
21
+ - '18.88'
22
+ - 'Checking'
23
+ - '107502429'
24
+ - '94217885428'
25
+ - '2010010'
26
+ -
27
+ - '00111282'
28
+ - 'Gretchen Gutierrez'
29
+ - '18.88'
30
+ - 'Savings'
31
+ - '342882012'
32
+ - '24254'
33
+ - '2010010'
34
+ -
35
+ - '00111795'
36
+ - 'Audrice Pitto'
37
+ - '18.88'
38
+ - 'Checking'
39
+ - '547107458'
40
+ - '280458548254'
41
+ - '2010010'
42
+ -
43
+ - '00111537'
44
+ - 'Rachel Ruppert'
45
+ - '18.88'
46
+ - 'Checking'
47
+ - '125480806'
48
+ - '641081508245'
49
+ - '2010010'
50
+ -
51
+ - '00111892'
52
+ - 'Judith Witlock'
53
+ - '18.88'
54
+ - 'Checking'
55
+ - '982458912'
56
+ - '52488542858'
57
+ - '2010010'
58
+ -
59
+ - '00111172'
60
+ - 'Kris Martell'
61
+ - '18.88'
62
+ - 'Savings'
63
+ - '108458068'
64
+ - '4728458248'
65
+ - '2010010'
66
+ -
67
+ - '00111284'
68
+ - 'Michell Penderson'
69
+ - '18.88'
70
+ - 'Checking'
71
+ - '645388043'
72
+ - '38184599459'
73
+ - '2010010'
74
+ -
75
+ - '00111756'
76
+ - 'Polly Clark'
77
+ - '18.88'
78
+ - 'Checking'
79
+ - '543882548'
80
+ - '543886028'
81
+ - '2010010'
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ ---
2
+ -
3
+ :username: testclient1
4
+ :password: test1
5
+ :company_alias: tester1
6
+ :company_username: testuser1
7
+ :company_password: fakeness1
8
+ :cache_location: ./EFT
9
+ -
10
+ :username: testclient2
11
+ :password: test2
12
+ :company_alias: tester2
13
+ :company_username: testuser2
14
+ :company_password: fakeness2
15
+ :cache_location: ./EFT
@@ -0,0 +1,11 @@
1
+ HD,tester,tester1,fakeness1,Check
2
+ CC,VISA,4508288028044788,12/2010,18.88,N,,,Kipp Owen,,,,,,,,100111532,Debit,,2,3,1,
3
+ CC,VISA,4979170284809229,12/2011,18.88,N,,,Brooke Racquet,,,,,,,,100111382,Debit,,2,3,1,
4
+ CC,MCRD,5280827180294022,12/2012,18.88,N,,,Pat Sylvester,,,,,,,,100111018,Debit,,2,3,1,
5
+ CC,MCRD,5173179138280429,12/2011,18.88,N,,,Vicki Tolliki,,,,,,,,100111992,Debit,,2,3,1,
6
+ CC,VISA,4247972792081828,12/2010,18.88,N,,,Cheryl Barger,,,,,,,,100111247,Debit,,2,3,1,
7
+ CC,MCRD,5428208180420824,12/2011,18.88,N,,,Melissa Fletcher,,,,,,,,100111028,Debit,,2,3,1,
8
+ CC,MCRD,5425682492949922,12/2012,18.88,N,,,Mack Payne,,,,,,,,100111384,Debit,,2,3,1,
9
+ CC,MCRD,5208372629781292,12/2011,18.88,N,,,Melissa Latcher,,,,,,,,100111742,Debit,,2,3,1,
10
+ CC,MCRD,5181381081538389,12/2012,18.88,N,,,Caroline Sieben,,,,,,,,100111402,Debit,,2,3,1,
11
+ CC,MCRD,5274727290818284,12/2011,18.88,N,,,Charlotte Sue Hiran,,,,,,,,100111208,Debit,,2,3,1,