ruby-saferpay 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,12 @@
1
+ == 0.0.2 / 2007-08-14
2
+
3
+ * 3 minor enhancement
4
+ * Inquiry
5
+ * Docs
6
+ * Tests
7
+
8
+ == 0.0.1 / 2007-08-12
9
+
10
+ * 1 major enhancement
11
+ * Basic SCAI
12
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/ruby-saferpay
6
+ lib/ruby-saferpay.rb
7
+ lib/ruby-saferpay/version.rb
8
+ test/test_ruby-saferpay.rb
data/README.txt ADDED
@@ -0,0 +1,218 @@
1
+ ruby-saferpay
2
+ = ELC-Tecnologies[http://www.elctech.com], http://www.elctech.com
3
+
4
+ == DESCRIPTION:
5
+
6
+ Saferpay (http://www.saferpay.com) is a european e-commerce payment services provider, present mostly in Switzerland, Germany and Austria but expanding elsewhere as well.
7
+ This gem provides a ruby interface to the "Saferpay Card Authorization Interface" (SCAI) part of the API. The code also contain a "payinit" method that is part of the "Virtual Terminal" (VT) approach to ecommerce payments.
8
+
9
+ The SCAI interface is used when the merchant wishes to keep the acquirer on her/his own website for the whole duration of the transaction (client payment details transits through *both* the merchant site and the saferpay database ) whereas VT implies a redirect to the saferpay site.
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ * supports both common credit cards and direct debit cards ("Lastschrift")
14
+ * support for VT style payments is incomplete
15
+
16
+
17
+ == SYNOPSIS:
18
+
19
+ Init (info from saferpay test account; they're the same for all test accounts):
20
+ @pan = "9451123100000004" # Saferpay test PAN
21
+ @accountid = "99867-94913159" # Saferpay test ACCOUNTID
22
+ @exp = "1107" # This will change for other test accounts I guess... Might just be three months ahead of Time.now
23
+ @sfp = Saferpay.new( @accountid, @pan, @exp )
24
+
25
+ Reserve:
26
+ <tt>@sfp.reserve(30000, "USD")</tt>
27
+
28
+ Amounts are divided by 100. We're talking cents here, not dollars...
29
+
30
+ Capture last transaction:
31
+ <tt>@sfp.capture</tt>
32
+
33
+ Capture with a transacaton ID "4hj34hj4hh34h4j3hj4h334":
34
+ <tt>@sfp.capture("4hj34hj4hh34h4j3hj4h334")</tt>
35
+
36
+ == REQUIREMENTS:
37
+
38
+ Saferpay:
39
+ * A saferpay account
40
+ * openssl-0.9.7: saferpay docs says "d" version, but "m" seem to work; 0.9.8 does _not_ work. MacPorts package: "openssl97"
41
+
42
+ * A working saferpay installation
43
+ As of today (august '07), the linux distribution have issues and compilation has not been straightforward (I have used the libidpapp lib from the binary distribution and compiled the saferpay executable from the source distribution, using
44
+ make -f saferpay.mk
45
+ The libs from the binary distribution require lbstdc++5 (not standard on most modern linux distros).
46
+
47
+ YMMV.
48
+
49
+ The Mac binary distribution is for OS X 10.2, so that's pretty useless today; the Mac source distribution is not compiling (and is outdated).
50
+
51
+ Use the linux source distribution with modified makefiles as follows:
52
+
53
+ Makefile:
54
+ PREFIX = /usr
55
+ SSLVERSION = openssl-0.9.7b
56
+ SSLEAYDIR = ../$(SSLVERSION)
57
+
58
+
59
+ all:
60
+
61
+ make -f idpapp.mk
62
+ cp ./out/libidpapp.dylib $(PREFIX)/lib
63
+
64
+ make -f saferpay.mk
65
+ cp idpapi.h ./out
66
+ cp idperrc.h ./out
67
+ cp ./out/settings.template ./out/settings.xml
68
+
69
+ install:
70
+ cp ./out/libidpapp.s* $(PREFIX)/lib
71
+
72
+ clean:
73
+ make -f idpapp.mk clean
74
+ make -f saferpay.mk clean
75
+
76
+ xs: all
77
+ perl -e 'system("cd perl/MessageObject\nperl Makefile.PL\nmake");'
78
+ perl -e 'system("cd perl/MessageFactory\nperl Makefile.PL\nmake");'
79
+ perl -e 'system("cd perl/ConfigurationSetup\nperl Makefile.PL\nmake");'
80
+
81
+ testxs:
82
+ perl -e 'system("cd perl/ConfigurationSetup\nmake test");'
83
+ perl -e 'system("cd perl/MessageFactory\nmake test");'
84
+
85
+ installxs:
86
+ perl -e 'system("cd perl/ConfigurationSetup\nmake install");'
87
+ perl -e 'system("cd perl/MessageFactory\nmake install");'
88
+ perl -e 'system("cd perl/MessageObject\nmake install");'
89
+
90
+ cleanxs:
91
+ perl -e 'system("cd perl/ConfigurationSetup\nmake clean");'
92
+ perl -e 'system("cd perl/MessageFactory\nmake clean");'
93
+ perl -e 'system("cd perl/MessageObject\nmake clean");'
94
+
95
+ testmk:
96
+ echo "SSLVERSION: $(SSLVERSION)"
97
+ echo "SSLEAYDIR: $(SSLEAYDIR)"
98
+
99
+
100
+ saferpay.mk:
101
+ ####### Compiler, tools and options
102
+ CC = gcc
103
+ CXX = g++
104
+
105
+ #CFLAGS = -pipe -fPIC -O2
106
+ CFLAGS = -pipe -fPIC -O2 -DUNIX -DHAVE_UNISTD_H -DHAVE_STDLIB_H
107
+ CXXFLAGS= -pipe -fPIC -O2
108
+ INCPATH =
109
+
110
+ LINK = g++
111
+ LFLAGS = -fPIC -ldl
112
+ LIBS = -lidpapp
113
+ MOC = $(QTDIR)/bin/moc
114
+
115
+ TAR = tar -cf
116
+ GZIP = gzip -9f
117
+
118
+ ####### Files
119
+
120
+ HEADERS = idpapi.h \
121
+ idperrc.h
122
+ SOURCES = saferpay.c
123
+ OBJECTS = saferpay.o
124
+ SRCMOC =
125
+ OBJMOC =
126
+ DIST =
127
+ TARGET = ./out/saferpay
128
+
129
+ ####### Implicit rules
130
+
131
+ .SUFFIXES: .cpp .cxx .cc .C .c
132
+
133
+ .cpp.o:
134
+ $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
135
+
136
+ .cxx.o:
137
+ $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
138
+
139
+ .cc.o:
140
+ $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
141
+
142
+ .C.o:
143
+ $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
144
+
145
+ .c.o:
146
+ $(CC) -c $(CFLAGS) $(INCPATH) -o $@ $<
147
+
148
+ ####### Build rules
149
+
150
+ all: $(TARGET)
151
+
152
+ $(TARGET): $(OBJECTS) $(OBJMOC)
153
+ $(LINK) $(LFLAGS) -o $(TARGET) $(OBJECTS) $(OBJMOC) $(LIBS)
154
+
155
+ moc: $(SRCMOC)
156
+
157
+ tmake: saferpay.mk
158
+
159
+ # no more tmake
160
+
161
+
162
+ dist:
163
+ $(TAR) saferpay.tar saferpay.pro $(SOURCES) $(HEADERS) $(DIST)
164
+ $(GZIP) saferpay.tar
165
+
166
+ clean:
167
+ -rm -f $(OBJECTS) $(OBJMOC) $(SRCMOC) $(TARGET)
168
+ -rm -f *~ core
169
+
170
+ ####### Compile
171
+
172
+ saferpay.o: saferpay.c \
173
+ idpapi.h \
174
+ idperrc.h
175
+
176
+ Ruby:
177
+ * Log4r is needed for logging (will log to GEMDIR/lib/log if user can write there)
178
+ * Hpricot is needed for XML parse (overkill?)
179
+
180
+ == INSTALL:
181
+ * saferpay tools (see docs and comments above)
182
+ * a working saferpay installation (run <tt>saferpay --help</tt> for more info and look at the Saferpay docs)
183
+ * <tt>sudo gem install ruby-saferpay</tt>
184
+ * test:
185
+ cd GEM_INSTALLATION_DIR
186
+ rake
187
+
188
+
189
+ The gem expects the saferpay installtion to be found in <tt>/opt/saferpay/</tt>. For the time being hack the source to change this:
190
+ BASEDIR = '/opt/saferpay/'
191
+ EXECUTABLE = 'saferpay'
192
+ CONFIG = BASEDIR
193
+
194
+
195
+ == LICENSE:
196
+
197
+ (The MIT License)
198
+
199
+ Copyright (c) 2007 ELC Tecnologies
200
+
201
+ Permission is hereby granted, free of charge, to any person obtaining
202
+ a copy of this software and associated documentation files (the
203
+ 'Software'), to deal in the Software without restriction, including
204
+ without limitation the rights to use, copy, modify, merge, publish,
205
+ distribute, sublicense, and/or sell copies of the Software, and to
206
+ permit persons to whom the Software is furnished to do so, subject to
207
+ the following conditions:
208
+
209
+ The above copyright notice and this permission notice shall be
210
+ included in all copies or substantial portions of the Software.
211
+
212
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
213
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
214
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
215
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
216
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
217
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
218
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/ruby-saferpay/version.rb'
4
+ require './lib/ruby-saferpay.rb'
5
+
6
+ Hoe.new('ruby-saferpay', RubySaferpay::VERSION::STRING) do |p|
7
+ p.rubyforge_name = 'ruby-saferpay'
8
+ p.author = 'David Palm (ELC Tecnologies)'
9
+ p.email = 'dpalm@elctech.com'
10
+ p.summary = 'Ruby interface to the saferpay e-commerce payment provider'
11
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
12
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ p.extra_deps << ['log4r']
15
+ p.extra_deps << ['hpricot']
16
+ end
data/bin/ruby-saferpay ADDED
File without changes
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by David Palm for ELC Tecnologies on 2007-08-12.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require 'log4r'
8
+ require 'net/https'
9
+ require 'uri'
10
+ require 'net/https'
11
+ require 'hpricot'
12
+
13
+ class Saferpay
14
+
15
+ class WireProtocolError <StandardError; end
16
+ class AttributeError <StandardError; end
17
+ class ConfigError <RuntimeError; end
18
+ # TODO: is there an eleant way of having the error messages "jump" out of here when a Transaction error is raised?
19
+ class TransactionError <StandardError
20
+
21
+ ERRORS = [
22
+ {:code => 0, :short_message => 'Authorization Successful', :long_message => ''},
23
+ {:code => 5, :short_message => 'Access Denied', :long_message => 'The access to the specified account was denied by Saferpay.'},
24
+ {:code => 21, :short_message => 'Invalid Structure', :long_message => 'Invalid structure of request.'},
25
+ {:code => 22, :short_message => 'Unknown Action', :long_message => 'Unknown action attribute.'},
26
+ {:code => 23, :short_message => 'Invalid Action', :long_message => 'Invalid action attribute or action not possible.'},
27
+ {:code => 61, :short_message => 'Invalid Card', :long_message => 'The static checks failed on this card (range check, LUHN check digit).'},
28
+ {:code => 62, :short_message => 'Invalid Date', :long_message => 'Invalid expiration date.'},
29
+ {:code => 63, :short_message => 'Card Expired', :long_message => 'The card has expired.'},
30
+ {:code => 64, :short_message => 'Unknown Card', :long_message => 'The card type is unknown the BIN range could not be assigned to a known card brand.'},
31
+ {:code => 65, :short_message => 'Authorization Denied', :long_message => 'The processor has denied the transaction request.'},
32
+ {:code => 67, :short_message => 'No Contract', :long_message => '"No contract exists for the card/currency combination specified.'},
33
+ {:code => 68, :short_message => 'Ambigous Contract', :long_message => '"More than one contracts exist for the specified card/currency combination.'},
34
+ {:code => 75, :short_message => 'Missing Parameter', :long_message => 'One or more mandatory parameters are missing'},
35
+ {:code => 76, :short_message => 'Connect Failed', :long_message => 'The connection to the card processor could not be established or was broken during the request. Retry the request.'},
36
+ {:code => 77, :short_message => 'No Endpoint', :long_message => 'No endpoint is specified for the processor of the card. This processor may not support online authorization of cards.'},
37
+ {:code => 78, :short_message => 'Internal Error', :long_message => 'A system error has occurred during processing the request. Retry the request if possible.'},
38
+ {:code => 80, :short_message => 'No Terminal', :long_message => 'Terminal does not exist.'},
39
+ {:code => 82, :short_message => 'Not Found', :long_message => 'Transaction not found.'},
40
+ {:code => 83, :short_message => 'Invalid Currency', :long_message => 'The specified currency code is invalid.'},
41
+ {:code => 84, :short_message => 'Invalid Amount', :long_message => 'The specified amout is invalid or does not match the rules for the currency.'},
42
+ {:code => 87, :short_message => 'Prevalidate Denied', :long_message => 'Access denied.'},
43
+ {:code => 88, :short_message => 'Reservation Invalid', :long_message => 'Reservation invalid.'},
44
+ {:code => 89, :short_message => 'Reservation Overbooked', :long_message => 'Amount of reservation overbooked.'},
45
+ {:code => 90, :short_message => 'Contract Disabled', :long_message => 'The contract for this card is currently disabled.'},
46
+ {:code => 97, :short_message => 'Already Captured', :long_message => 'Transaction already captured (PayComplete)'},
47
+ {:code => 98, :short_message => 'Invalid Signature', :long_message => 'Invalid signature'},
48
+ {:code => 102, :short_message => 'Not Supported', :long_message => 'Function not supported by provider.'},
49
+ {:code => 104, :short_message => 'Denied Blacklist', :long_message => 'Card number in customer black list.'},
50
+ {:code => 105, :short_message => 'Denied Country', :long_message => 'Card number not in country BIN range list.'},
51
+ {:code => 151, :short_message => 'Timeout Response', :long_message => 'Timeout waiting on authorization response. Retry the request.'},
52
+ {:code => 152, :short_message => 'Unknown Error', :long_message => 'Unknown (system) error.'},
53
+ {:code => 301, :short_message => 'Authentication Error', :long_message => 'An error happened during the authentication request. The merchant application could choose to:\n- continue the payment without authentication or\n- ask customer for other payment method or\n- stop the payment.'}
54
+ ]
55
+ attr_reader :error
56
+ def initialize(saferpay_reply)
57
+ if saferpay_reply.is_a? String
58
+ result = Hpricot.parse(cmd_result).at('idp').attributes['result'].to_i rescue 152
59
+ elsif saferpay_reply.is_a? Hash
60
+ result = saferpay_reply['result'].to_i rescue 152
61
+ elsif saferpay_reply.is_a? Fixnum
62
+ result = saferpay_reply
63
+ else
64
+ result = 152 # Unknown error
65
+ end
66
+ @error = ERRORS.any?{|err| err[:code] == result}
67
+ end
68
+ end
69
+
70
+ LOGNAME = File.basename(__FILE__, '.rb')
71
+ LOGDIR = File.dirname(__FILE__)+'/log'
72
+
73
+ # TODO: re-work this. Use a ".saferpayrc" file with config info?
74
+ BASEDIR = '/opt/saferpay/'
75
+ EXECUTABLE = 'saferpay'
76
+ CONFIG = BASEDIR
77
+
78
+ # Supported currencies. Please note that these are the ones Saferpay supports;
79
+ # and not necessarily the ones your account support. You might encounter "unsupported
80
+ # currency" errors even if the currency is among the following.
81
+ # (Test account supports at least USD, EUR, CHF)
82
+ CURRENCIES = ['CHF','CZK','DKK','EUR','GBP','PLN','SEK','USD']
83
+
84
+ attr_accessor :account_id
85
+ attr_reader :current_transaction
86
+
87
+ def initialize( account_id = "99867-94913159", pan = "9451123100000004", expiry_date = nil, cvc = nil, name = nil, tolerance = 0 )
88
+ Saferpay.check_install
89
+ @current_transaction = nil
90
+
91
+ @account_id = account_id
92
+ @pan = pan
93
+ @expiry_date= expiry_date
94
+ @cvc = cvc
95
+ @name = name
96
+ @tolerance = tolerance # Amount tolerance in percent. The finally captured amount is AMOUNT + TOLERANCE
97
+ end
98
+
99
+ # Convenience method to:
100
+ # 1. Authorize (reserve)
101
+ # 2. Capture
102
+ # 3. Batch clear (needed/wanted? Probably not...)
103
+ def pay(amount, currency = 'EUR' )
104
+ reserve amount, currency
105
+ capture
106
+ details
107
+ end
108
+
109
+ def details( transaction_id = nil )
110
+ transaction_id = @current_transaction.transaction_id if transaction_id.nil?
111
+ cmd = "#{BASEDIR}#{EXECUTABLE} -exec -p #{CONFIG} -m Inquiry -a ID #{transaction_id} -a TYPE Transaction -a TOKEN \"(not used)\""
112
+ #logger.debug "#{self.class}#details COMMAND: #{cmd}"
113
+ do_direct_cc cmd
114
+ end
115
+
116
+ def reserve( amount, currency )
117
+ check_params amount, currency
118
+ @current_transaction = TransactionParams.new(
119
+ :amount => amount,
120
+ :currency => currency,
121
+ :accountid => @account_id,
122
+ :pan => @pan,
123
+ :exp => @expiry_date
124
+ )
125
+ do_direct_cc
126
+ end
127
+
128
+ def refund( amount, currency )
129
+ check_params amount, currency
130
+ @current_transaction = TransactionParams.new(
131
+ :amount => amount,
132
+ :currency => currency,
133
+ :accountid => @account_id,
134
+ :pan => @pan,
135
+ :exp => @expiry_date,
136
+ :action => "Credit"
137
+ )
138
+ do_direct_cc
139
+ end
140
+
141
+ def refund_last
142
+ refund_transaction @current_transaction.transaction_id
143
+ end
144
+
145
+ def refund_transaction( transaction_id )
146
+ cmd = "#{BASEDIR}#{EXECUTABLE} -capt -p #{CONFIG} -i #{transaction_id} -t \"(not used)\""
147
+ do_direct_cc cmd
148
+ end
149
+
150
+ # Capture the amount of the transaction with id transaction_id
151
+ # Defaults to capturing the current transaction's amount.
152
+ def capture( transaction_id = nil, token = nil, params = nil )
153
+ transaction_id = @current_transaction.transaction_id if transaction_id.nil?
154
+ token = @current_transaction.token if token.nil?
155
+ cmd = "#{BASEDIR}#{EXECUTABLE} -capt -p #{CONFIG} -i #{transaction_id} -t \"#{token}\" #{params}"
156
+ do_direct_cc cmd
157
+ end
158
+
159
+ def cancel( transaction_id = nil, token = nil )
160
+ transaction_id = @current_transaction.transaction_id if transaction_id.nil?
161
+ token = @current_transaction.token if token.nil?
162
+ params = "-a ACTION Cancel"
163
+
164
+ capture transaction_id, token, params
165
+ end
166
+
167
+ # Direct debit on aquierer's bank account.
168
+ # This will always fail with the test account.
169
+ # Takes an account number (10 digits) and a mysterious 'BLZ' number (8 digits).
170
+ # This is a very common payment method in Germany and Austria
171
+ def debit_card_reserve( amount, currency, account_number, blz )
172
+ check_params amount, currency
173
+ raise AttributeError, "Account number is a ten digit number" unless account_number.length == 10
174
+ raise AttributeError, "BLZ is a eight digit number" unless blz.length == 8
175
+ @current_transaction = TransactionParams.new(
176
+ :amount => amount,
177
+ :currency => currency,
178
+ :accountid => @account_id,
179
+ :track2 => "\";59#{blz}=#{account_number}\""
180
+ )
181
+ do_direct_cc
182
+ end
183
+ alias :lastschrift :debit_card_reserve
184
+
185
+ # Create and send a payinit message. For use with the Virtual Terminal (VT).
186
+ # Unfinished (meaning: it works, but need tests and some love and also a
187
+ # couple of more methods to handle the paycomplete and other types of messages.
188
+ # Take it for what it is: a stub)
189
+ def payinit( amount, currency, description, backlink = 'http://localhost', faillink = 'http://localhost', successlink = 'http://localhost', notifyurl = 'http://localhost')
190
+ @current_transaction = TransactionParams.new(
191
+ :amount => amount,
192
+ :currency => currency,
193
+ :description => description,
194
+ :accountid => @account_id,
195
+ :backlink => backlink,
196
+ :faillink => faillink,
197
+ :successlink => successlink,
198
+ :notifyurl => notifyurl
199
+ )
200
+ cmd = "#{BASEDIR}#{EXECUTABLE} -payinit -p #{CONFIG} -a #{@current_transaction.to_s}"
201
+ logger.debug "#{self.class}#payinit Will execute command:\n#{cmd}"
202
+ payinit = %x{#{cmd} 2>&1}
203
+
204
+ logger.debug "#{self.class}#payinit result: #{payinit.inspect}"
205
+
206
+ unless $?.success?
207
+ logger.error "#{self.class}#payinit FAILED error status: #{$?.inspect}"
208
+ return false
209
+ else
210
+ logger.debug "#{self.class}#payinit Saferpay URI generated"
211
+ end
212
+
213
+ uri = URI.parse( payinit )
214
+ logger.debug "#{self.class}#payinit Built an uri object. Host: #{uri.host}, port: #{uri.port}"
215
+ http = Net::HTTP.new( uri.host, uri.port )
216
+ http.use_ssl = true
217
+ result = http.start do |http|
218
+ logger.debug "#{self.class}#payinit GETting uri: #{uri.request_uri}"
219
+ http.get(uri.request_uri)
220
+ end
221
+ unless result.is_a? Net::HTTPOK
222
+ logger.error "#{self.class}#payinit Cannot reach saferpay site."
223
+ # TODO: Should re-raise here?
224
+ return false
225
+ end
226
+
227
+ # TODO: this suck
228
+ if result.body =~ /missing ACCOUNTID attribute/
229
+ raise AttributeError, 'Missing ACCOUNTID attribute'
230
+ end
231
+ case result.body
232
+ when /missing ACCOUNTID attribute/
233
+ raise AttributeError, 'Missing ACCOUNTID attribute'
234
+ when /missing BACKLINK attribute/
235
+ raise AttributeError, 'Missing BACKLINK attribute'
236
+ when /missing FAILLINK attribute/
237
+ raise AttributeError, 'Missing FAILLINK attribute'
238
+ when /missing SUCCESSLINK|NOTIFYURL attribute/
239
+ raise AttributeError, 'Missing SUCCESSLINK|NOTIFYURL attribute'
240
+ end
241
+
242
+ result
243
+ end
244
+
245
+ private
246
+
247
+ # Execute the saferpay ommand. Defaults to the most common type of command.
248
+ # TODO: should return a Transaction object
249
+ def do_direct_cc( cmd = nil )
250
+ cmd = "#{BASEDIR}#{EXECUTABLE} -exec -p #{CONFIG} -m Authorization -a #{@current_transaction.to_s}" if cmd.nil?
251
+ cmd_result = %x{#{cmd} 2>&1}
252
+
253
+ unless $?.success?
254
+ logger.error "#{self.class}#do_direct_cc FAILED \nError status: #{$?.inspect}\nCommand:#{cmd}\nCommand result: #{cmd_result}"
255
+ raise TransactionError.new(cmd_result)
256
+ else
257
+ unless cmd_result.empty?
258
+ #logger.debug "#{self.class}#do_direct_cc Got a result:\n\t#{cmd_result}\n\tCommand: #{cmd}\n-------------------\n"
259
+ result = Hpricot.parse(cmd_result).at('idp').attributes # A hash
260
+ if result['result'] === "0"
261
+ @current_transaction.transaction_id = result['id']
262
+ @current_transaction.token = result['token']
263
+ elsif result['msgtype'] == 'InquiryResponse'
264
+ # TODO: check for valid InquiryResponse message
265
+ else
266
+ logger.error "#{self.class}#do_direct_cc FAILED \nError status: #{$?.inspect}\nCommand:#{cmd}\nCommand result: #{cmd_result}"
267
+ raise TransactionError.new(result)
268
+ end
269
+ else
270
+ result = true
271
+ end
272
+ end
273
+ result
274
+ end
275
+
276
+ def check_params(amount, currency) #:nodoc:
277
+ raise ArgumentError, "Amount has to be an integer" unless amount.is_a? Fixnum
278
+ raise ArgumentError, "Amount has to be a positive integer" unless amount > 1
279
+ raise ArgumentError, "Currency has to be string" unless currency.is_a? String
280
+ raise AttributeError, "Unsupported currency" unless CURRENCIES.include? currency
281
+ end
282
+
283
+ def logger # :nodoc:
284
+ @@loger ||= Saferpay.setup_logging
285
+ end
286
+
287
+ def self.logger #:nodoc:
288
+ @@loger ||= Saferpay.setup_logging
289
+ end
290
+
291
+ # Setup logging for class
292
+ def self.setup_logging #:nodoc:
293
+ @@logger = Log4r::Logger.new LOGNAME
294
+ @@logger.outputters << Log4r::Outputter.stdout
295
+ begin
296
+ op = Log4r::FileOutputter.new LOGNAME, :filename => "#{LOGDIR}/#{LOGNAME}.log", :trunc => false
297
+ op.formatter = Log4r::PatternFormatter.new(:pattern => "[%c, %d, %l] :: %M")
298
+ @@logger.outputters << op
299
+ rescue StandardError
300
+ @@logger.debug("CANNOT WRITE LOGFILE TO \"#{LOGDIR}/#{LOGNAME}.log\". Proceeding anyway.")
301
+ end
302
+ @@logger
303
+ end
304
+
305
+ # Check installation
306
+ def self.check_install #:nodoc:
307
+ unless File.exist?(BASEDIR+EXECUTABLE)
308
+ raise ConfigError, "No saferpay executable in \"#{BASEDIR}#{EXECUTABLE}\""
309
+ end
310
+
311
+ unless File.executable?(BASEDIR+EXECUTABLE)
312
+ raise ConfigError, "Saferpay binary in \"#{BASEDIR}#{EXECUTABLE}\" is not executable"
313
+ end
314
+
315
+ result = %x{#{BASEDIR+EXECUTABLE} --help}
316
+ # saferpay --help has an exit code of "1", so can't check for success...
317
+ #unless $?.success?
318
+ unless result =~ /^SAFERPAY COMMAND LINE UTILITY/
319
+ raise ConfigError, "Cannot execute \"#{BASEDIR+EXECUTABLE} --help\". Result: #{result}\nExit code: #{$?.inspect}"
320
+ end
321
+
322
+ unless File.exist?(BASEDIR+'config.xml')
323
+ raise ConfigError, "No config.xml file. Need to run \"saferpay -config\" maybe?"
324
+ end
325
+
326
+ # Should contain: <IDP MSGTYPE="SetupResponse" GXID="0A5365AB-BCBD-4D72-88C3-32BFD5F09912" CUSTOMERID="99867" VERSION="1" VTAUTOURL="https://www.saferpay.com/user/setup.asp" VTURL="https://www.saferpay.com/vt/Pay.asp" VTKEYID="1-0" CAPTUREURL="https://www.saferpay.com/vt/capture.asp" VTSCRIPTURL="http://www.saferpay.com/OpenSaferpayScript.asp"/>
327
+ conf = File.read(BASEDIR+'config.xml')
328
+ unless conf =~ /<IDP MSGTYPE="SetupResponse"/
329
+ raise ConfigError, "config.xml looks borked"
330
+ end
331
+
332
+ unless File.directory? BASEDIR+'keys'
333
+ raise ConfigError, "No keys directory; configuration needed"
334
+ end
335
+
336
+ unless File.exist?(BASEDIR+'keys/current')
337
+ raise ConfigError, "No current key in keys directory"
338
+ end
339
+
340
+ true
341
+ end
342
+
343
+ class TransactionParams
344
+ DEFAULTS = {}
345
+
346
+ attr_accessor :params, :transaction_id, :token
347
+ def initialize( params )
348
+ @params = DEFAULTS.merge( params )
349
+ @transaction_id = nil
350
+ @token = nil
351
+ end
352
+
353
+ def to_s
354
+ @params.map{|k,v| "#{k.to_s.upcase} #{v}"}.join " -a "
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,9 @@
1
+ module RubySaferpay #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 2
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,162 @@
1
+ require 'yaml'
2
+ require 'tmpdir'
3
+ require 'test/unit'
4
+ require 'lib/ruby-saferpay.rb'
5
+
6
+ class SaferpayTest < Test::Unit::TestCase
7
+ def setup
8
+ @pan = "9451123100000004" # Saferpay test PAN
9
+ @accountid = "99867-94913159" # Saferpay test ACCOUNTID
10
+ @exp = "1107" # This will change for other test accounts I guess... Might just be three months ahead of Time.now
11
+ @sfp = Saferpay.new( @accountid, @pan, @exp )
12
+ @sfp2 = Saferpay.new( @accountid, @pan, @exp )
13
+ end
14
+
15
+ def test_install
16
+ assert_nothing_raised { Saferpay.check_install }
17
+ end
18
+
19
+ def test_reserve
20
+ assert_nothing_raised{
21
+ res = @sfp.reserve( 300, "USD" )
22
+ assert_is_authorization_response res
23
+ }
24
+ end
25
+
26
+ def test_reserve_with_bad_params
27
+ assert_raise( ArgumentError ) { @sfp.reserve( 300 ) }
28
+ assert_raise( ArgumentError ) { @sfp.reserve( "300", "USD" ) }
29
+ assert_raise( ArgumentError ) { @sfp.reserve( "300", "300" ) }
30
+ assert_raise( Saferpay::AttributeError ) { @sfp.reserve( 300, "OOO" ) }
31
+ end
32
+
33
+ def test_reserve_with_suported_currency_but_not_in_contract
34
+ assert_raise( Saferpay::TransactionError ) {
35
+ res = @sfp.reserve( 300, "SEK" )
36
+ assert_is_authorization_response res
37
+ }
38
+ end
39
+
40
+ def test_refund
41
+ res = @sfp.reserve( 350, "CHF" )
42
+ assert_is_authorization_response res
43
+ end
44
+
45
+ def test_refund_with_bad_params
46
+ assert_raise( ArgumentError ) { @sfp.refund( 300 ) }
47
+ assert_raise( ArgumentError ) { @sfp.refund( "300", "USD" ) }
48
+ assert_raise( ArgumentError ) { @sfp.refund( "300", "300" ) }
49
+ assert_raise( Saferpay::AttributeError ) { @sfp.refund( 300, "OOO" ) }
50
+ assert_nothing_raised{ @sfp.refund( 10,"EUR" ) }
51
+ end
52
+
53
+ def test_refund_last
54
+ @sfp.reserve( 1000, 'USD' )
55
+ assert @sfp.refund_last
56
+
57
+ assert_raise( NoMethodError ) { @sfp2.refund_last }
58
+ end
59
+
60
+ def test_capture
61
+ @sfp.reserve( 600, 'EUR' )
62
+ assert @sfp.capture
63
+
64
+ assert_raise( NoMethodError ) { @sfp2.capture }
65
+ end
66
+
67
+ def test_cancel
68
+ @sfp.reserve( 600, 'EUR' )
69
+ @sfp.capture
70
+ assert @sfp.cancel
71
+ end
72
+
73
+ def test_debit_card_reserve
74
+ assert_raise( ArgumentError ) { @sfp.debit_card_reserve }
75
+ assert_raise( Saferpay::TransactionError ) {
76
+ @sfp.debit_card_reserve(100200, "EUR", "1234567890", "12345678")
77
+ assert_is_authorization_response res
78
+ }
79
+ end
80
+
81
+ def test_details
82
+ assert_raise ( NoMethodError ) { @sfp.details }
83
+ assert_nothing_raised{
84
+ @sfp.reserve( 3000, "USD" )
85
+ @sfp.capture
86
+ @sfp.details
87
+ }
88
+
89
+ assert_nothing_raised{
90
+ res = @sfp.reserve( 1000, "USD" )
91
+ @sfp.capture
92
+
93
+ res2 = @sfp.reserve( 1100, "USD" )
94
+ @sfp.capture
95
+
96
+ assert_not_equal @sfp.details( res['id'] ), @sfp.details( res2['id'] )
97
+ assert_not_equal @sfp.details( res['id'] ), @sfp.details
98
+ }
99
+ end
100
+
101
+ def test_pay
102
+ res = @sfp.pay( 4000 )
103
+ assert_is_inquiry_response res
104
+ end
105
+
106
+
107
+ private
108
+ def assert_is_inquiry_response(res)
109
+ assert_instance_of Hash, res
110
+ assert_is_saferpay_response res
111
+ require_these = [:pan, :track2, :exp, :cccountry, :cvc, :accountid, :contractnumber, :amount, :currency, :providerid, :providername, :authcode,:authmessage, :authdate, :application, :eci, :settled, :settledate, :action, :cancelled, :closed]
112
+ assert require_these.all?{ |param| res.has_key? param.to_s }, "Required parameter is missing in response: #{res.inspect}"
113
+ assert res['msgtype'] == "InquiryResponse", "Msgtype is not \"InquiryResponse\""
114
+ end
115
+
116
+ #<IDP MSGTYPE="InquiryResponse"
117
+ # ID="rItIQfb77lnztAAUfW5Cb6zUr1fA"
118
+ # PAN="9451123100000004"
119
+ # TRACK2=";9451123100000004=0711?0"
120
+ # EXP="0711"
121
+ # CCCOUNTRY="XX"
122
+ # CVC="no"
123
+ # ACCOUNTID="99867-94913159"
124
+ # CONTRACTNUMBER="123456789"
125
+ # AMOUNT="4000"
126
+ # CURRENCY="EUR"
127
+ # PROVIDERID="90"
128
+ # PROVIDERNAME="Saferpay Test Card"
129
+ # AUTHCODE="745000"
130
+ # AUTHMESSAGE=""
131
+ # AUTHDATE="20070814 12:04:08"
132
+ # APPLICATION="Saferpay Card Authorization Interface"
133
+ # ECI="0"
134
+ # SETTLED="yes"
135
+ # SETTLEDATE="20070814 12:04:09"
136
+ # ACTION="Debit"
137
+ # CANCELLED="no"
138
+ # CLOSED="no"
139
+ #/>
140
+ def assert_is_authorization_response(res)
141
+ assert_instance_of Hash, res
142
+ assert_is_saferpay_response res
143
+ assert res['msgtype'] == "AuthorizationResponse", "Msgtype is not \"AuthorizationResponse\""
144
+ assert res.has_key?( 'result' ), "No result code received in response"
145
+ assert res['result'] == "0", "Result code is not Zero (\"0\")"
146
+ assert res.has_key?( 'token' ), "No token in response"
147
+ assert res['token'] == "(unused)", "Token is not \"(unused)\""
148
+ end
149
+
150
+ def assert_is_saferpay_response(res)
151
+ assert res.has_key?( 'accountid' ), "No accountid received in response"
152
+ assert res['accountid'] == @accountid, "Accountid is not the test accountid"
153
+ assert res.has_key?( 'msgtype' ), "No msgtype in response"
154
+ assert res.has_key?( 'authcode' ), "No authcode in response"
155
+ assert res.has_key?( 'id' ), "No id in response"
156
+ assert res.has_key?( 'providerid' ), "No providerid in response"
157
+ assert res.has_key?( 'providername' ), "No providername in response"
158
+ assert res.has_key?( 'cccountry' ), "No cccountry in response"
159
+ assert res.has_key?( 'contractnumber' ), "No contractnumber in response"
160
+ end
161
+
162
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: ruby-saferpay
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.2
7
+ date: 2007-08-15 00:00:00 +02:00
8
+ summary: Ruby interface to the saferpay e-commerce payment provider
9
+ require_paths:
10
+ - lib
11
+ email: dpalm@elctech.com
12
+ homepage: = ELC-Tecnologies[http://www.elctech.com], http://www.elctech.com
13
+ rubyforge_project: ruby-saferpay
14
+ description: "The SCAI interface is used when the merchant wishes to keep the acquirer on her/his own website for the whole duration of the transaction (client payment details transits through *both* the merchant site and the saferpay database ) whereas VT implies a redirect to the saferpay site. == FEATURES/PROBLEMS: * supports both common credit cards and direct debit cards (\"Lastschrift\") * support for VT style payments is incomplete == SYNOPSIS: Init (info from saferpay test account; they're the same for all test accounts): @pan = \"9451123100000004\" # Saferpay test PAN @accountid = \"99867-94913159\" # Saferpay test ACCOUNTID @exp = \"1107\" # This will change for other test accounts I guess... Might just be three months ahead of Time.now @sfp = Saferpay.new( @accountid, @pan, @exp ) Reserve: <tt>@sfp.reserve(30000, \"USD\")</tt> Amounts are divided by 100. We're talking cents here, not dollars... Capture last transaction: <tt>@sfp.capture</tt> Capture with a transacaton ID \"4hj34hj4hh34h4j3hj4h334\": <tt>@sfp.capture(\"4hj34hj4hh34h4j3hj4h334\")</tt> == REQUIREMENTS:"
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - David Palm (ELC Tecnologies)
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/ruby-saferpay
37
+ - lib/ruby-saferpay.rb
38
+ - lib/ruby-saferpay/version.rb
39
+ - test/test_ruby-saferpay.rb
40
+ test_files:
41
+ - test/test_ruby-saferpay.rb
42
+ rdoc_options:
43
+ - --main
44
+ - README.txt
45
+ extra_rdoc_files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - README.txt
49
+ executables:
50
+ - ruby-saferpay
51
+ extensions: []
52
+
53
+ requirements: []
54
+
55
+ dependencies:
56
+ - !ruby/object:Gem::Dependency
57
+ name: log4r
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Version::Requirement
60
+ requirements:
61
+ - - ">"
62
+ - !ruby/object:Gem::Version
63
+ version: 0.0.0
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: hpricot
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.0.0
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: hoe
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.0
82
+ version: