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 +12 -0
- data/Manifest.txt +8 -0
- data/README.txt +218 -0
- data/Rakefile +16 -0
- data/bin/ruby-saferpay +0 -0
- data/lib/ruby-saferpay.rb +357 -0
- data/lib/ruby-saferpay/version.rb +9 -0
- data/test/test_ruby-saferpay.rb +162 -0
- metadata +82 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
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,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:
|