solon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +19 -0
- data/README.textile +143 -0
- data/lib/solon/basket.rb +15 -0
- data/lib/solon/basket_item.rb +11 -0
- data/lib/solon/config.rb +6 -0
- data/lib/solon/customer_data.rb +16 -0
- data/lib/solon/error.rb +4 -0
- data/lib/solon/money.rb +10 -0
- data/lib/solon/net/https_gateway.rb +80 -0
- data/lib/solon/post_data.rb +11 -0
- data/lib/solon/protx_callback.rb +34 -0
- data/lib/solon/protx_response.rb +84 -0
- data/lib/solon/protx_services.rb +51 -0
- data/lib/solon/unique_id.rb +7 -0
- data/lib/solon_gateway.rb +397 -0
- data/solon.gemspec +18 -0
- data/spec/solon_gateway_spec.rb +113 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +6 -0
- metadata +88 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 by Alastair Brunton, Daniel Turner
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
h1. Solon - Sagepay VSP Server
|
2
|
+
|
3
|
+
h2. Introduction
|
4
|
+
|
5
|
+
Solon is a library for accessing the Sagepay VSP Server. Its patterns are loosely modeled on "ActiveMerchant":http://www.activemerchant.org/, for clarity (unfortunately AM doesn't support the VSP Server mode of operation). Solon was built for an application called "Woobius":https://www.woobius.com, but is released free of charge (and free of any warranties) to the community in the hope that it will be useful.
|
6
|
+
|
7
|
+
Using the Sagepay VSP Server enables you to not have to host payment pages on your own site. You initiate a transaction, then forward the user to the page returned by Sagepay, and the rest is handled off-site. This has many benefits, in particular not needing to implement credit card validation pages yourself, not needing to handle credit card numbers directly (which means that you're automatically PCI-DSS compliant), and not needing to deal with 3D-secure yourself.
|
8
|
+
|
9
|
+
Sagepay is an UK payment gateway. You can find more information on their VPS Server service "here":http://techsupport.Sagepay.com/vspservercustom.asp.
|
10
|
+
|
11
|
+
h2. Licence
|
12
|
+
|
13
|
+
The initial version of Solon was written from scratch by Daniel Tenner. It is loosely based on ActiveMerchant's patterns, but all code is original, and is licenced under the "Apache Licence":http://www.oss-watch.ac.uk/resources/apache2.xml.
|
14
|
+
|
15
|
+
As a user, this means you can effectively use Solon for whatever you want so long as you agree not to sue people about it.
|
16
|
+
|
17
|
+
As a contributor, it means that you licence to Solon any contributions that you make as per the Apache licence.
|
18
|
+
|
19
|
+
h2. Kicking the tyres
|
20
|
+
|
21
|
+
After setting up your IP address in the VSP Simulator, and obtaining a valid simulator vendor name, temporarily edit <code>simulate.rb</code> with your *vendor name* and run:
|
22
|
+
|
23
|
+
<pre>
|
24
|
+
ruby simulate.rb
|
25
|
+
</pre>
|
26
|
+
|
27
|
+
Everything should work. If it doesn't work, try asking me (swombat) for help on freenode, channel #flails.
|
28
|
+
|
29
|
+
Assuming everything worked fine, revert your change to simulate.rb.
|
30
|
+
|
31
|
+
h2. Usage
|
32
|
+
|
33
|
+
h3. Configuration
|
34
|
+
|
35
|
+
In each environment file, you want to do something like this:
|
36
|
+
|
37
|
+
<pre>
|
38
|
+
PEEVES_VENDOR = "woobius"
|
39
|
+
PEEVES_GATEWAY_MODE = :simulator
|
40
|
+
</pre>
|
41
|
+
|
42
|
+
The gateway mode can be set to <code>:simulator</code>, <code>:test</code>, or <code>:live</code>.
|
43
|
+
|
44
|
+
Then, in environment.rb, do:
|
45
|
+
|
46
|
+
<pre>
|
47
|
+
Solon::Config.vendor = PEEVES_VENDOR
|
48
|
+
Solon::Config.gateway_mode = PEEVES_GATEWAY_MODE
|
49
|
+
</pre>
|
50
|
+
|
51
|
+
h3. Making a request
|
52
|
+
|
53
|
+
As per <code>simulate.rb</code>:
|
54
|
+
|
55
|
+
<pre>
|
56
|
+
transaction_reference = Solon::UniqueId.generate("TEST")
|
57
|
+
customer_data = Solon::CustomerData.new(:surname => 'blah',
|
58
|
+
:firstnames => 'blah',
|
59
|
+
:address1 => 'blah',
|
60
|
+
:address2 => 'blah',
|
61
|
+
:city => 'blah',
|
62
|
+
:post_code => 'blah',
|
63
|
+
:country => 'gb',
|
64
|
+
:email => 'customer@email.com'
|
65
|
+
)
|
66
|
+
|
67
|
+
# Payment registration
|
68
|
+
payment_response = p.payment(Solon::Money.new(1000, "GBP"),
|
69
|
+
{
|
70
|
+
:transaction_reference => transaction_reference,
|
71
|
+
:description => "Test Transaction",
|
72
|
+
:notification_url => "http://callback.example.com/process_stuff",
|
73
|
+
:customer_data => { :billing => customer_data,
|
74
|
+
:delivery => customer_data,
|
75
|
+
:email => customer_data.email }
|
76
|
+
})
|
77
|
+
</pre>
|
78
|
+
|
79
|
+
This will register a payment and return a url that you should forward the user to.
|
80
|
+
|
81
|
+
Once the user has made the payment on the Sagepay VSP Server pages, you will receive a callback at the URL you defined as <code>:notification_url</code>. You should call something like the following on the parameters that you receive back:
|
82
|
+
|
83
|
+
<pre>
|
84
|
+
def record_response_params(params)
|
85
|
+
returning SolonGateway.parse_notification(params) do |response|
|
86
|
+
self.update_attributes(
|
87
|
+
:last_status => response.status,
|
88
|
+
:last_status_detail => response.last_status_detail,
|
89
|
+
:vps_transaction_id => response.vps_transaction_id,
|
90
|
+
:transaction_authorisation_number => response.transaction_authorisation_number,
|
91
|
+
:status_3d_secure => response.status_3d_secure,
|
92
|
+
:code_3d_secure => response.code_3d_secure
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
</pre>
|
97
|
+
|
98
|
+
In response to this, you must send back a success message or the transaction will eventually be cancelled by Sagepay:
|
99
|
+
|
100
|
+
<pre>
|
101
|
+
render :text => SolonGateway.response(SolonGateway::APPROVED, url, "Payment succeeded")
|
102
|
+
</pre>
|
103
|
+
|
104
|
+
Sagepay will then forward the user to <code>url</code>, where you can display a happy success page.
|
105
|
+
|
106
|
+
h3. Repeat transactions
|
107
|
+
|
108
|
+
Sagepay VSP Server wouldn't be worth much without repeat transactions. Here's an example of how to use them (this code sits on an Invoice class in Woobius):
|
109
|
+
|
110
|
+
<pre>
|
111
|
+
def pay_from(previous_invoice)
|
112
|
+
response = SolonGateway.new.repeat(Solon::Money.new(self.currency_amount, self.currency),
|
113
|
+
{
|
114
|
+
:transaction_reference => self.reference,
|
115
|
+
:description => self.description,
|
116
|
+
:related_transaction_reference => previous_invoice.reference,
|
117
|
+
:related_vps_transaction_id => previous_invoice.vps_transaction_id,
|
118
|
+
:related_security_key => previous_invoice.security_key,
|
119
|
+
:related_transaction_authorisation_number => previous_invoice.transaction_authorisation_number
|
120
|
+
})
|
121
|
+
self.update_attributes(
|
122
|
+
:last_status => response.status,
|
123
|
+
:last_status_detail => response.status_detail,
|
124
|
+
:vps_transaction_id => response.vps_transaction_id,
|
125
|
+
:security_key => response.security_key
|
126
|
+
)
|
127
|
+
if response.approved?
|
128
|
+
self.paid!
|
129
|
+
end
|
130
|
+
|
131
|
+
response
|
132
|
+
end
|
133
|
+
</pre>
|
134
|
+
|
135
|
+
h3. Keeping abreast of changes
|
136
|
+
|
137
|
+
Things might change. If you pull the latest version of Solon and something doesn't work, check the changelog: "CHANGES.textile":http://github.com/swombat/peeves/tree/master/CHANGES.textile.
|
138
|
+
|
139
|
+
h2. Contributing back
|
140
|
+
|
141
|
+
Please use the fork functionality on github to make a fork and then push back your changes to the fork queue. I will probably accept most useful changes, but it might take me a few days before I get around to it!
|
142
|
+
|
143
|
+
Thanks!
|
data/lib/solon/basket.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module Solon
|
2
|
+
class BasketItem
|
3
|
+
attr_accessor :description, :quantity, :unit_cost_without_tax, :tax_applied, :unit_cost_with_tax, :total_cost_with_tax
|
4
|
+
|
5
|
+
def to_post_data
|
6
|
+
"#{description}:#{quantity}:#{unit_cost_with_tax}:#{tax_applied}:#{unit_cost_with_tax}:#{total_cost_with_tax}"
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :to_s, :to_post_data
|
10
|
+
end
|
11
|
+
end
|
data/lib/solon/config.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Solon
|
2
|
+
class CustomerData
|
3
|
+
# attr_accessor :address, :post_code, :name, :contact_number, :email
|
4
|
+
attr_accessor :surname, :firstnames, :address1, :address2, :city, :post_code, :country, :state, :phone, :email
|
5
|
+
|
6
|
+
def initialize(attrs = {})
|
7
|
+
attrs.each do |h,v|
|
8
|
+
send("#{h.to_s}=", v)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](arg)
|
13
|
+
send("#{arg}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/solon/error.rb
ADDED
data/lib/solon/money.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module Solon
|
4
|
+
module Net
|
5
|
+
|
6
|
+
class ConnectionError < Solon::Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class RetriableConnectionError < Solon::Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class HttpsGateway
|
13
|
+
|
14
|
+
MAX_RETRIES = 3
|
15
|
+
OPEN_TIMEOUT = 60
|
16
|
+
READ_TIMEOUT = 60
|
17
|
+
|
18
|
+
def initialize(url, retry_safe=false, debug=false)
|
19
|
+
@url = url
|
20
|
+
@retry_safe = retry_safe
|
21
|
+
@debug = debug
|
22
|
+
end
|
23
|
+
|
24
|
+
def retry_safe?
|
25
|
+
@retry_safe
|
26
|
+
end
|
27
|
+
|
28
|
+
def debug?
|
29
|
+
@debug
|
30
|
+
end
|
31
|
+
|
32
|
+
def send(headers, data)
|
33
|
+
headers['Content-Type'] ||= "application/x-www-form-urlencoded"
|
34
|
+
|
35
|
+
uri = URI.parse(@url)
|
36
|
+
|
37
|
+
http = ::Net::HTTP.new(uri.host, uri.port)
|
38
|
+
http.open_timeout = OPEN_TIMEOUT
|
39
|
+
http.read_timeout = READ_TIMEOUT
|
40
|
+
|
41
|
+
http.set_debug_output $stdout if debug?
|
42
|
+
|
43
|
+
http.use_ssl = true
|
44
|
+
|
45
|
+
http.verify_mode = ::OpenSSL::SSL::VERIFY_NONE
|
46
|
+
|
47
|
+
retry_exceptions do
|
48
|
+
begin
|
49
|
+
http.post(uri.request_uri, data, headers).body
|
50
|
+
rescue EOFError => e
|
51
|
+
raise ConnectionError, "The remote server dropped the connection"
|
52
|
+
rescue Errno::ECONNRESET => e
|
53
|
+
raise ConnectionError, "The remote server reset the connection"
|
54
|
+
rescue Errno::ECONNREFUSED => e
|
55
|
+
raise RetriableConnectionError, "The remote server refused the connection"
|
56
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
57
|
+
raise ConnectionError, "The connection to the remote server timed out"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def retry_exceptions
|
64
|
+
retries = MAX_RETRIES
|
65
|
+
begin
|
66
|
+
yield
|
67
|
+
rescue RetriableConnectionError => e
|
68
|
+
retries -= 1
|
69
|
+
puts e.inspect
|
70
|
+
retry unless retries.zero?
|
71
|
+
raise ConnectionError, e.message
|
72
|
+
rescue ConnectionError
|
73
|
+
retries -= 1
|
74
|
+
retry if retry_safe? && !retries.zero?
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Solon
|
2
|
+
class SagepayCallback < SagepayResponse
|
3
|
+
def initialize(response)
|
4
|
+
@response = response
|
5
|
+
if @response.is_a?(String)
|
6
|
+
response.split("&").each do |line|
|
7
|
+
key, *value = line.split("=")
|
8
|
+
value = value.join("=")
|
9
|
+
self[key] = value
|
10
|
+
end
|
11
|
+
elsif @response.is_a?(Hash)
|
12
|
+
response.each do |key, value|
|
13
|
+
self[key] = value
|
14
|
+
end
|
15
|
+
else
|
16
|
+
raise Solon::Error, "Cannot parse response of type #{@response.class}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def failed?
|
21
|
+
SolonGateway::FAILURES.include?(self.status.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
def error?
|
25
|
+
SolonGateway::ERRORS.include?(self.status.to_sym)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Generic status checker method
|
29
|
+
def st(status_check)
|
30
|
+
status_check.to_s == self.status.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
module Solon
|
3
|
+
class SagepayResponse
|
4
|
+
def initialize(response)
|
5
|
+
RAILS_DEFAULT_LOGGER.debug "Sagepay response: #{response}"
|
6
|
+
@response = response
|
7
|
+
if @response.is_a?(String)
|
8
|
+
response.split("\r\n").each do |line|
|
9
|
+
key, *value = line.split("=")
|
10
|
+
value = value.join("=")
|
11
|
+
self[key] = value
|
12
|
+
end
|
13
|
+
elsif @response.is_a?(Hash)
|
14
|
+
response.each do |key, value|
|
15
|
+
self[key] = value
|
16
|
+
end
|
17
|
+
else
|
18
|
+
raise Solon::Error, "Cannot parse response of type #{@response.class}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(id, *args)
|
23
|
+
id = id.to_s
|
24
|
+
@values ||= {}
|
25
|
+
|
26
|
+
case id[-1]
|
27
|
+
when 61: # :blah=
|
28
|
+
@values[id[0..-2].to_sym] = args[0]
|
29
|
+
when 63: # :blah?
|
30
|
+
@values.has_key?(id[0..-2].to_sym)
|
31
|
+
else # :blah
|
32
|
+
@values[id.to_sym]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(key, value)
|
37
|
+
self.send("#{mapping[key] || key}=", CGI.unescape(value))
|
38
|
+
end
|
39
|
+
|
40
|
+
def [](key)
|
41
|
+
self.send("#{mapping[key] || key}")
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# TODO: Make this work, currently fails all
|
46
|
+
def verify!
|
47
|
+
return self
|
48
|
+
md5 = Digest::MD5.new
|
49
|
+
md5 << "#{self.vps_transaction_id}#{self.transaction_reference}#{self.status}#{self.transaction_authorisation_number}" +
|
50
|
+
"#{Solon::Config.vendor}#{self.avs_cv2_result}#{self.security_key}#{self.address_result}#{self.post_code_result}" +
|
51
|
+
"#{self.cv2_result}#{self.gift_aid}#{self.status_3d_secure}#{self.code_3d_secure}"
|
52
|
+
|
53
|
+
raise Solon::Error, "MD5 appears to have been tampered with! (#{md5.hexdigest} != #{self.vps_signature})" unless md5.hexdigest == self.vps_signature
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def approved?
|
59
|
+
self.status == SolonGateway::APPROVED
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def mapping
|
64
|
+
{
|
65
|
+
"VPSProtocol" => :vps_protocol,
|
66
|
+
"Status" => :status,
|
67
|
+
"StatusDetail" => :status_detail,
|
68
|
+
"VPSTxId" => :vps_transaction_id,
|
69
|
+
"SecurityKey" => :security_key,
|
70
|
+
"NextURL" => :next_url,
|
71
|
+
"TxAuthNo" => :transaction_authorisation_number,
|
72
|
+
"AVSCV2" => :avs_cv2_result,
|
73
|
+
"AddressResult" => :address_result,
|
74
|
+
"PostCodeResult" => :post_code_result,
|
75
|
+
"CV2Result" => :cv2_result,
|
76
|
+
"VendorTxCode" => :transaction_reference,
|
77
|
+
"GiftAid" => :gift_aid,
|
78
|
+
"3DSecureStatus" => :status_3d_secure,
|
79
|
+
"CAVV" => :code_3d_secure,
|
80
|
+
"VPSSignature" => :vps_signature
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Solon
|
2
|
+
module SagepayServices
|
3
|
+
BASE_URL = {
|
4
|
+
:simulator => 'https://test.sagepay.com/simulator/VSPServerGateway.asp',
|
5
|
+
:test => 'https://test.sagepay.com/gateway/service/',
|
6
|
+
:live => 'https://live.sagepay.com/gateway/service/'
|
7
|
+
}
|
8
|
+
|
9
|
+
SERVICE = {
|
10
|
+
:simulator => {
|
11
|
+
:payment => '?Service=VendorRegisterTx',
|
12
|
+
:deferred => '?Service=VendorRegisterTx',
|
13
|
+
:release => '?Service=VendorReleaseTx',
|
14
|
+
:abort => '?Service=VendorAbortTx',
|
15
|
+
:refund => '?Service=VendorRefundTx',
|
16
|
+
:repeat => '?Service=VendorRepeatTx',
|
17
|
+
:void => '?Service=VendorVoidTx',
|
18
|
+
:manual => lambda { raise Solon::Error, "MANUAL transactions not supported in simulator mode" },
|
19
|
+
:directrefund => lambda { raise Solon::Error, "DIRECTREFUND transactions not supported in simulator mode" },
|
20
|
+
:authorise => '?Service=VendorAuthoriseTx',
|
21
|
+
:cancel => '?Service=VendorCancelTx'
|
22
|
+
},
|
23
|
+
:test => {
|
24
|
+
:payment => 'vspserver-register.vsp',
|
25
|
+
:deferred => 'vspserver-register.vsp',
|
26
|
+
:release => 'release.vsp',
|
27
|
+
:abort => 'abort.vsp',
|
28
|
+
:refund => 'refund.vsp',
|
29
|
+
:repeat => 'repeat.vsp',
|
30
|
+
:void => 'void.vsp',
|
31
|
+
:manual => 'manual.vsp',
|
32
|
+
:directrefund => 'directrefund.vsp',
|
33
|
+
:authorise => 'authorise.vsp',
|
34
|
+
:cancel => 'cancel.vsp'
|
35
|
+
},
|
36
|
+
:live => {
|
37
|
+
:payment => 'vspserver-register.vsp',
|
38
|
+
:deferred => 'vspserver-register.vsp',
|
39
|
+
:release => 'release.vsp',
|
40
|
+
:abort => 'abort.vsp',
|
41
|
+
:refund => 'refund.vsp',
|
42
|
+
:repeat => 'repeat.vsp',
|
43
|
+
:void => 'void.vsp',
|
44
|
+
:manual => 'manual.vsp',
|
45
|
+
:directrefund => 'directrefund.vsp',
|
46
|
+
:authorise => 'authorise.vsp',
|
47
|
+
:cancel => 'cancel.vsp'
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,397 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
class SolonGateway
|
4
|
+
include Solon::SagepayServices
|
5
|
+
|
6
|
+
APPROVED = 'OK'
|
7
|
+
INVALID = 'INVALID'
|
8
|
+
NOTAUTHED = 'NOTAUTHED'
|
9
|
+
ABORT = 'ABORT'
|
10
|
+
REJECTED = 'REJECTED'
|
11
|
+
ERROR = 'ERROR'
|
12
|
+
ABORT_REPEATED = 'ABORT REPEATED'
|
13
|
+
|
14
|
+
FAILURES = [:NOTAUTHED, :ABORT, :REJECTED, :ABORT_REPEATED]
|
15
|
+
ERRORS = [:INVALID, :ERROR]
|
16
|
+
|
17
|
+
|
18
|
+
VPS_PROTOCOL = "2.23"
|
19
|
+
|
20
|
+
TRANSACTIONS = {
|
21
|
+
:payment => 'PAYMENT',
|
22
|
+
:authenticate => 'AUTHENTICATE',
|
23
|
+
:authorise => 'AUTHORISE',
|
24
|
+
:deferred => 'DEFERRED',
|
25
|
+
:refund => 'REFUND',
|
26
|
+
:release => 'RELEASE',
|
27
|
+
:repeat => 'REPEAT',
|
28
|
+
:void => 'VOID',
|
29
|
+
:cancel => 'CANCEL',
|
30
|
+
:abort => 'ABORT'
|
31
|
+
}
|
32
|
+
|
33
|
+
AVS_CVV_CODE = {
|
34
|
+
"NOTPROVIDED" => nil,
|
35
|
+
"NOTCHECKED" => 'X',
|
36
|
+
"MATCHED" => 'Y',
|
37
|
+
"NOTMATCHED" => 'N'
|
38
|
+
}
|
39
|
+
|
40
|
+
def initialize(mode=Solon::Config.gateway_mode)
|
41
|
+
@mode = mode
|
42
|
+
end
|
43
|
+
|
44
|
+
def debug=(value)
|
45
|
+
@no_debug = !value
|
46
|
+
end
|
47
|
+
|
48
|
+
def debug?
|
49
|
+
@mode == :simulator && !@no_debug
|
50
|
+
end
|
51
|
+
|
52
|
+
# Registers a payment so that the user can enter their details.
|
53
|
+
# Returns a URL that the user must be forwarded to to make the payment. If the process following this request is
|
54
|
+
# followed (i.e. redirect to next_url, user fills in their details, Sagepay notifies server, and server responds with OK)
|
55
|
+
# the settlement will go out the next day, automatically, for the full amount.
|
56
|
+
# Options:
|
57
|
+
# => transaction_reference (required, String(40))
|
58
|
+
# => description (required, String(100))
|
59
|
+
# => notification_url (required, String)
|
60
|
+
# => billing_data (optional, BillingData)
|
61
|
+
# => basket (optional, Basket)
|
62
|
+
# Response:
|
63
|
+
# => status
|
64
|
+
# => status_detail
|
65
|
+
# => vps_transaction_id
|
66
|
+
# => security_key
|
67
|
+
# => next_url
|
68
|
+
def payment(money, options)
|
69
|
+
log "payment", options
|
70
|
+
|
71
|
+
add_common TRANSACTIONS[:payment]
|
72
|
+
add_registration(money, options)
|
73
|
+
add_customer(options)
|
74
|
+
add_basket(options)
|
75
|
+
|
76
|
+
commit! :payment
|
77
|
+
end
|
78
|
+
|
79
|
+
# Registers a payment so that the user can enter their details.
|
80
|
+
# Returns a URL that the user must be forwarded to to make the payment. If the process following this request is
|
81
|
+
# followed (i.e. redirect to next_url, user fills in their details, Sagepay notifies server, and server responds with OK)
|
82
|
+
# the settlement will NOT go out the next day. Further "Authorise" requests are required before the user will be
|
83
|
+
# charged.
|
84
|
+
# Options:
|
85
|
+
# => transaction_reference (required, String(40))
|
86
|
+
# => description (required, String(100))
|
87
|
+
# => notification_url (required, String)
|
88
|
+
# => billing_data (optional, BillingData)
|
89
|
+
# => basket (optional, Basket)
|
90
|
+
# Response:
|
91
|
+
# => status
|
92
|
+
# => status_detail
|
93
|
+
# => vps_transaction_id
|
94
|
+
# => security_key
|
95
|
+
# => next_url
|
96
|
+
def authenticate(money, options)
|
97
|
+
log "authenticate", options
|
98
|
+
|
99
|
+
add_common(TRANSACTIONS[:authenticate])
|
100
|
+
add_registration(money, options)
|
101
|
+
add_customer(options)
|
102
|
+
add_basket(options)
|
103
|
+
|
104
|
+
commit! :payment
|
105
|
+
end
|
106
|
+
|
107
|
+
# Registers a payment so that the user can enter their details.
|
108
|
+
# Returns a URL that the user must be forwarded to to make the payment. If the process following this request is
|
109
|
+
# followed (i.e. redirect to next_url, user fills in their details, Sagepay notifies server, and server responds with OK)
|
110
|
+
# the settlement will NOT go out the next day, but a "shadow" will be placed on the account until the deferred payment
|
111
|
+
# is "Released" or "Aborted". Deferred payments are only supposed to be used to add a delay of up to 7 days, to allow
|
112
|
+
# cahrging at the point when the order is shipped.
|
113
|
+
# Options:
|
114
|
+
# => transaction_reference (required, String(40))
|
115
|
+
# => description (required, String(100))
|
116
|
+
# => notification_url (required, String)
|
117
|
+
# => billing_data (optional, BillingData)
|
118
|
+
# => basket (optional, Basket)
|
119
|
+
# Response:
|
120
|
+
# => status
|
121
|
+
# => status_detail
|
122
|
+
# => vps_transaction_id
|
123
|
+
# => security_key
|
124
|
+
# => next_url
|
125
|
+
def deferred(money, options)
|
126
|
+
log "deferred", options
|
127
|
+
|
128
|
+
add_common TRANSACTIONS[:deferred]
|
129
|
+
add_registration(money, options)
|
130
|
+
add_customer(options)
|
131
|
+
add_basket(options)
|
132
|
+
|
133
|
+
commit! :deferred
|
134
|
+
end
|
135
|
+
|
136
|
+
# Submits a repeat transaction.
|
137
|
+
# Options:
|
138
|
+
# => transaction_reference (required, String(40))
|
139
|
+
# => description (required, String(100))
|
140
|
+
# => related_transaction_reference (required, String(40))
|
141
|
+
# => related_vps_transaction_id (required, String(38))
|
142
|
+
# => related_security_key (required, String(10))
|
143
|
+
# => related_transaction_authorisation_number (required, Long Integer)
|
144
|
+
# Response:
|
145
|
+
# => status
|
146
|
+
# => status_detail
|
147
|
+
# => vps_transaction_id
|
148
|
+
# => transaction_authorisation_number
|
149
|
+
# => security_key
|
150
|
+
def repeat(money, options)
|
151
|
+
log "repeat", options
|
152
|
+
|
153
|
+
add_common(TRANSACTIONS[:repeat])
|
154
|
+
add_related(options)
|
155
|
+
add_registration(money, options)
|
156
|
+
|
157
|
+
commit! :repeat
|
158
|
+
end
|
159
|
+
|
160
|
+
# Authorises a previously authenticated transaction. This can be done multiple times, for amounts adding up to
|
161
|
+
# a maximum of 115% of the authenticated amount.
|
162
|
+
# => transaction_reference (required, String(40))
|
163
|
+
# => description (required, String(100))
|
164
|
+
# => related_transaction_reference (required, String(40))
|
165
|
+
# => related_vps_transaction_id (required, String(38))
|
166
|
+
# => related_security_key (required, String(10))
|
167
|
+
# Response:
|
168
|
+
# => status
|
169
|
+
# => status_detail
|
170
|
+
# => vps_transaction_id
|
171
|
+
# => transaction_authorisation_number
|
172
|
+
# => security_key
|
173
|
+
# => avs_cv2_result
|
174
|
+
# => address_result
|
175
|
+
# => post_code_result
|
176
|
+
# => cv2_result
|
177
|
+
def authorise(money, options)
|
178
|
+
log "authorise", options
|
179
|
+
|
180
|
+
add_common TRANSACTIONS[:authorise]
|
181
|
+
add_related(options)
|
182
|
+
add_registration(money, options)
|
183
|
+
|
184
|
+
commit! :authorise
|
185
|
+
end
|
186
|
+
|
187
|
+
# Cancels a previously authenticated transaction.
|
188
|
+
# Options:
|
189
|
+
# => transaction_reference (required, String(40))
|
190
|
+
# => vps_transaction_id (required, String(38))
|
191
|
+
# => security_key (required, String(10))
|
192
|
+
# Response:
|
193
|
+
# => status
|
194
|
+
# => status_detail
|
195
|
+
def cancel(options)
|
196
|
+
log "cancel", options
|
197
|
+
|
198
|
+
add_common TRANSACTIONS[:cancel]
|
199
|
+
add_post_processing(options)
|
200
|
+
|
201
|
+
commit! :cancel
|
202
|
+
end
|
203
|
+
|
204
|
+
# Releases (processes for payment/settlement) a DEFERRED or REPEATDEFERRED payment.
|
205
|
+
# Options:
|
206
|
+
# => transaction_reference (required, String(40))
|
207
|
+
# => vps_transaction_id (required, String(38))
|
208
|
+
# => security_key (required, String(10))
|
209
|
+
# => transaction_authorisation_number (required, Long Integer)
|
210
|
+
# Response:
|
211
|
+
# => status
|
212
|
+
# => status_detail
|
213
|
+
def release(money, options)
|
214
|
+
log "release", options
|
215
|
+
|
216
|
+
add_common TRANSACTIONS[:release]
|
217
|
+
add_post_processing(options)
|
218
|
+
@post["ReleaseAmount"] = "%.2f" % money.amount
|
219
|
+
|
220
|
+
commit! :release
|
221
|
+
end
|
222
|
+
|
223
|
+
# Voids a previously authorised payment (only possible after receiving the NotificationURL response).
|
224
|
+
# Options:
|
225
|
+
# => transaction_reference (required, String(40))
|
226
|
+
# => vps_transaction_id (required, String(38))
|
227
|
+
# => security_key (required, String(10))
|
228
|
+
# => transaction_authorisation_number (required, Long Integer)
|
229
|
+
# Response:
|
230
|
+
# => status
|
231
|
+
# => status_detail
|
232
|
+
def void(options)
|
233
|
+
log "void", options
|
234
|
+
|
235
|
+
add_common(TRANSACTIONS[:void])
|
236
|
+
add_post_processing(options)
|
237
|
+
|
238
|
+
commit! :void
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
# Aborts (cancels) a DEFERRED payment.
|
243
|
+
# Options:
|
244
|
+
# => transaction_reference (required, String(40))
|
245
|
+
# => vps_transaction_id (required, String(38))
|
246
|
+
# => security_key (required, String(10))
|
247
|
+
# => transaction_authorisation_number (required, Long Integer)
|
248
|
+
# Response:
|
249
|
+
# => status
|
250
|
+
# => status_detail
|
251
|
+
def abort(options)
|
252
|
+
log "abort", options
|
253
|
+
|
254
|
+
add_common(TRANSACTIONS[:abort])
|
255
|
+
add_post_processing(options)
|
256
|
+
|
257
|
+
commit! :abort
|
258
|
+
end
|
259
|
+
|
260
|
+
# Refunds a previously settled transaction.
|
261
|
+
# Options:
|
262
|
+
# => transaction_reference (required, String(40))
|
263
|
+
# => description (required, String(100))
|
264
|
+
# => related_transaction_reference (required, String(40))
|
265
|
+
# => related_vps_transaction_id (required, String(38))
|
266
|
+
# => related_security_key (required, String(10))
|
267
|
+
# => related_transaction_authorisation_number (required, Long Integer)
|
268
|
+
# Response:
|
269
|
+
# => status
|
270
|
+
# => status_detail
|
271
|
+
# => vps_transaction_id
|
272
|
+
# => transaction_authorisation_number
|
273
|
+
def refund(money, options)
|
274
|
+
log "refund", options
|
275
|
+
|
276
|
+
add_common(TRANSACTIONS[:refund])
|
277
|
+
add_related(options)
|
278
|
+
add_registration(money, options)
|
279
|
+
|
280
|
+
commit! :refund
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.parse_notification(params)
|
284
|
+
Solon::SagepayResponse.new(params).verify!
|
285
|
+
end
|
286
|
+
|
287
|
+
def self.parse_callback(params)
|
288
|
+
Solon::SagepayCallback.new(params).verify!
|
289
|
+
end
|
290
|
+
|
291
|
+
def self.response(status, redirect_url, status_detail)
|
292
|
+
"Status=#{status}\r\n" +
|
293
|
+
"RedirectURL=#{redirect_url}\r\n" +
|
294
|
+
"StatusDetail=#{status_detail}"
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
def url_for(action)
|
299
|
+
BASE_URL[@mode] + SERVICE[@mode][action]
|
300
|
+
end
|
301
|
+
|
302
|
+
def commit!(action)
|
303
|
+
RAILS_DEFAULT_LOGGER.debug "Sending Sagepay post to #{url_for(action)}:\n" +
|
304
|
+
"Post: #{@post.inspect}\n" +
|
305
|
+
"Post data: #{@post.to_post_data}"
|
306
|
+
response = Solon::Net::HttpsGateway.new(url_for(action), true, debug?).send({}, @post.to_post_data)
|
307
|
+
Solon::SagepayResponse.new(response)
|
308
|
+
end
|
309
|
+
|
310
|
+
def add_common(type)
|
311
|
+
@post = Solon::PostData.new
|
312
|
+
@post["TxType"] = type
|
313
|
+
@post["VPSProtocol"] = VPS_PROTOCOL
|
314
|
+
@post["Vendor"] = Solon::Config.vendor
|
315
|
+
end
|
316
|
+
|
317
|
+
def add_post_processing(options)
|
318
|
+
@post["VendorTxCode"] = options[:transaction_reference][0..39]
|
319
|
+
@post["VPSTxId"] = options[:vps_transaction_id][0..37]
|
320
|
+
@post["SecurityKey"] = options[:security_key][0..9]
|
321
|
+
@post["TxAuthNo"] = options[:transaction_authorisation_number] unless options[:transaction_authorisation_number].nil?
|
322
|
+
end
|
323
|
+
|
324
|
+
def add_related(options)
|
325
|
+
@post["RelatedVendorTxCode"] = options[:related_transaction_reference][0..39]
|
326
|
+
@post["RelatedVPSTxId"] = options[:related_vps_transaction_id][0..37]
|
327
|
+
@post["RelatedSecurityKey"] = options[:related_security_key][0..9]
|
328
|
+
@post["RelatedTxAuthNo"] = options[:related_transaction_authorisation_number] unless options[:related_transaction_authorisation_number].nil?
|
329
|
+
end
|
330
|
+
|
331
|
+
def add_registration(money, options)
|
332
|
+
@post["VendorTxCode"] = options[:transaction_reference][0..39]
|
333
|
+
@post["Amount"] = "%.2f" % money.amount
|
334
|
+
@post["Currency"] = money.currency
|
335
|
+
@post["Description"] = options[:description][0..99]
|
336
|
+
@post["NotificationURL"] = options[:notification_url][0..254] unless options[:notification_url].nil?
|
337
|
+
end
|
338
|
+
|
339
|
+
def add_customer(options)
|
340
|
+
unless options[:customer_data].nil?
|
341
|
+
@post["CustomerEMail"] = options[:customer_data][:email]
|
342
|
+
add_billing(options[:customer_data])
|
343
|
+
add_delivery(options[:customer_data])
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def add_billing(options)
|
348
|
+
unless options[:billing].nil?
|
349
|
+
# removed character limitation, as they should be enforced in view
|
350
|
+
@post["BillingSurname"] = options[:billing].surname
|
351
|
+
@post["BillingFirstnames"] = options[:billing].firstnames
|
352
|
+
@post["BillingAddress1"] = options[:billing].address1
|
353
|
+
@post["BillingAddress2"] = options[:billing].address2
|
354
|
+
@post["BillingCity"] = options[:billing].city
|
355
|
+
@post["BillingPostCode"] = options[:billing].post_code
|
356
|
+
@post["BillingCountry"] = options[:billing].country
|
357
|
+
@post["BillingPhone"] = options[:billing].phone
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def add_delivery(options)
|
362
|
+
unless options[:delivery].nil?
|
363
|
+
# removed character limitation, as they should be enforced in view
|
364
|
+
@post["DeliverySurname"] = options[:delivery].surname
|
365
|
+
@post["DeliveryFirstnames"] = options[:delivery].firstnames
|
366
|
+
@post["DeliveryAddress1"] = options[:delivery].address1
|
367
|
+
@post["DeliveryAddress2"] = options[:delivery].address2
|
368
|
+
@post["DeliveryCity"] = options[:delivery].city
|
369
|
+
@post["DeliveryPostCode"] = options[:delivery].post_code
|
370
|
+
@post["DeliveryCountry"] = options[:delivery].country
|
371
|
+
@post["DeliveryPhone"] = options[:delivery].phone
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def add_basket(options)
|
376
|
+
unless options[:basket].nil?
|
377
|
+
@post["Basket"] = options[:basket].to_post_data
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def log(method, options)
|
382
|
+
RAILS_DEFAULT_LOGGER.debug "Called #{method} with options #{options.inspect}"
|
383
|
+
end
|
384
|
+
|
385
|
+
def requires!(hash, *params)
|
386
|
+
params.each do |param|
|
387
|
+
if param.is_a?(Array)
|
388
|
+
raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first)
|
389
|
+
|
390
|
+
valid_options = param[1..-1]
|
391
|
+
raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:connector => 'or')}") unless valid_options.include?(hash[param.first])
|
392
|
+
else
|
393
|
+
raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
data/solon.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib/', __FILE__)
|
3
|
+
$:.unshift lib unless $:.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "solon"
|
7
|
+
s.version = "0.0.1"
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Alastair Brunton"]
|
10
|
+
s.email = ["info@simplyexcited.co.uk"]
|
11
|
+
s.homepage = "http://github.com/pyrat/solon"
|
12
|
+
s.summary = "Interface to Sagepay payment processor"
|
13
|
+
s.description = "Interface for Sagepay VSP Server only."
|
14
|
+
|
15
|
+
s.required_rubygems_version = ">= 1.3.1"
|
16
|
+
s.require_path = 'lib'
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe SolonGateway do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@p = SolonGateway.new(:simulator)
|
7
|
+
@p.debug = false
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "sending a payment request" do
|
11
|
+
before(:each) do
|
12
|
+
customer_data = Solon::CustomerData.new(:surname => 'blah',
|
13
|
+
:firstnames => 'blah',
|
14
|
+
:address1 => 'blah',
|
15
|
+
:address2 => 'blah',
|
16
|
+
:city => 'blah',
|
17
|
+
:post_code => 'blah',
|
18
|
+
:country => 'gb'
|
19
|
+
)
|
20
|
+
|
21
|
+
@response = @p.payment(Solon::Money.new(1000, "GBP"),
|
22
|
+
{
|
23
|
+
:transaction_reference => Solon::UniqueId.generate("TEST"),
|
24
|
+
:description => "Test Transaction",
|
25
|
+
:notification_url => "http://test.example.com",
|
26
|
+
:customer_data => {:billing => customer_data, :delivery =>customer_data} ,
|
27
|
+
})
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return a SagepayResponse" do
|
31
|
+
@response.is_a?(Solon::SagepayResponse).should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have a vps_transaction_id" do
|
35
|
+
@response.vps_transaction_id.should_not be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a security_key" do
|
39
|
+
@response.security_key.should_not be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not have a transaction_authorisation_number" do
|
43
|
+
@response.transaction_authorisation_number.should be_nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# commented out
|
48
|
+
# AUTHORISE can only be used after AUTHENTICATE, not PAYMENT
|
49
|
+
# describe "sending an authenticate request" do
|
50
|
+
# before(:each) do
|
51
|
+
# pending
|
52
|
+
# @transaction_reference = Solon::UniqueId.generate("TEST")
|
53
|
+
# @response = @p.authenticate Solon::Money.new(1000, "GBP"),
|
54
|
+
# {
|
55
|
+
# :transaction_reference => @transaction_reference,
|
56
|
+
# :description => "Test Transaction",
|
57
|
+
# :notification_url => "http://test.example.com"
|
58
|
+
# }
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# it "should return a SagepayResponse" do
|
62
|
+
# @response.is_a?(Solon::SagepayResponse).should be_true
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# it "should have a vps_transaction_id" do
|
66
|
+
# @response.vps_transaction_id.should_not be_nil
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# it "should have a security_key" do
|
70
|
+
# @response.security_key.should_not be_nil
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# it "should not have a transaction_authorisation_number" do
|
74
|
+
# @response.transaction_authorisation_number.should be_nil
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# it "should be cancellable" do
|
78
|
+
# @response2 = @p.cancel({
|
79
|
+
# :transaction_reference => @transaction_reference,
|
80
|
+
# :vps_transaction_id => @response.vps_transaction_id,
|
81
|
+
# :security_key => @response.security_key
|
82
|
+
# })
|
83
|
+
# @response2.status.should == SolonGateway::APPROVED
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
|
87
|
+
describe "receiving a notification" do
|
88
|
+
before(:each) do
|
89
|
+
params = {
|
90
|
+
"Status"=>"OK",
|
91
|
+
"TxType"=>"PAYMENT",
|
92
|
+
"VPSTxId"=>"{861A2DB0-E734-4DEB-8F8B-12C47B9ADF3E}",
|
93
|
+
"VendorTxCode"=>"W-TEST-1227524828.86576-59414",
|
94
|
+
"GiftAid"=>"0",
|
95
|
+
"AVSCV2"=>"ALL MATCH",
|
96
|
+
"TxAuthNo"=>"8661",
|
97
|
+
"VPSProtocol"=>"2.22",
|
98
|
+
"CAVV"=>"MNL2CYF4URE47IQNBI6DAH",
|
99
|
+
"3DSecureStatus"=>"OK",
|
100
|
+
"VPSSignature"=>"49A6FA9FE0631919D9B1E72ACE57584D",
|
101
|
+
"CV2Result"=>"MATCHED",
|
102
|
+
"PostCodeResult"=>"MATCHED",
|
103
|
+
"AddressResult"=>"MATCHED"
|
104
|
+
}
|
105
|
+
@result = SolonGateway.parse_notification(params)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should be able to parse the notification into a Sagepay Response" do
|
109
|
+
@result.is_a?(Solon::SagepayResponse).should be_true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Alastair Brunton
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-14 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Interface for Sagepay VSP Server only.
|
23
|
+
email:
|
24
|
+
- info@simplyexcited.co.uk
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- LICENSE.txt
|
33
|
+
- README.textile
|
34
|
+
- lib/solon/basket.rb
|
35
|
+
- lib/solon/basket_item.rb
|
36
|
+
- lib/solon/config.rb
|
37
|
+
- lib/solon/customer_data.rb
|
38
|
+
- lib/solon/error.rb
|
39
|
+
- lib/solon/money.rb
|
40
|
+
- lib/solon/net/https_gateway.rb
|
41
|
+
- lib/solon/post_data.rb
|
42
|
+
- lib/solon/protx_callback.rb
|
43
|
+
- lib/solon/protx_response.rb
|
44
|
+
- lib/solon/protx_services.rb
|
45
|
+
- lib/solon/unique_id.rb
|
46
|
+
- lib/solon_gateway.rb
|
47
|
+
- solon.gemspec
|
48
|
+
- spec/solon_gateway_spec.rb
|
49
|
+
- spec/spec.opts
|
50
|
+
- spec/spec_helper.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/pyrat/solon
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 25
|
75
|
+
segments:
|
76
|
+
- 1
|
77
|
+
- 3
|
78
|
+
- 1
|
79
|
+
version: 1.3.1
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.3.7
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Interface to Sagepay payment processor
|
87
|
+
test_files: []
|
88
|
+
|