dcas-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/API.rdoc +44 -0
- data/HISTORY.rdoc +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/dcas-ruby.gemspec +87 -0
- data/lib/dcas/ach_response.rb +83 -0
- data/lib/dcas/ach_return.rb +133 -0
- data/lib/dcas/payment.rb +147 -0
- data/lib/dcas/response.rb +123 -0
- data/lib/dcas.rb +140 -0
- data/lib/net/ftps_implicit.rb +225 -0
- data/spec/dcas/response_spec.rb +11 -0
- data/spec/dcas_spec.rb +50 -0
- data/spec/fixtures/ach_first_response.csv.sample +0 -0
- data/spec/fixtures/ach_payments.csv +11 -0
- data/spec/fixtures/ach_payments.yml +81 -0
- data/spec/fixtures/ach_second_response.csv.sample +0 -0
- data/spec/fixtures/cc_response.csv.sample +0 -0
- data/spec/fixtures/clients.yml.sample +15 -0
- data/spec/fixtures/credit_card_payments.csv +11 -0
- data/spec/fixtures/credit_card_payments.yml +71 -0
- data/spec/fixtures/test_upload0.txt +0 -0
- data/spec/fixtures/test_upload1.txt +0 -0
- data/spec/fixtures/test_upload2.txt +0 -0
- data/spec/fixtures/test_upload3.txt +0 -0
- data/spec/fixtures/test_upload4.txt +0 -0
- data/spec/fixtures/test_upload5.txt +0 -0
- data/spec/fixtures/test_upload6.txt +0 -0
- data/spec/fixtures/test_upload7.txt +0 -0
- data/spec/fixtures/test_upload8.txt +0 -0
- data/spec/fixtures/test_upload9.txt +0 -0
- data/spec/ftps_implicit_spec.rb +48 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +18 -0
- metadata +115 -0
@@ -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
|
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,
|