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.
- 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,
|