sage_pay 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +10 -27
- data/lib/sage_pay/server/address.rb +45 -0
- data/lib/sage_pay/server/signature_verification_details.rb +14 -0
- data/lib/sage_pay/server/transaction_code.rb +18 -0
- data/lib/sage_pay/server/transaction_notification.rb +169 -0
- data/lib/sage_pay/server/transaction_notification_response.rb +62 -0
- data/lib/sage_pay/server/transaction_registration.rb +176 -0
- data/lib/sage_pay/server/transaction_registration_response.rb +94 -0
- data/lib/sage_pay/server.rb +31 -0
- data/lib/sage_pay.rb +20 -2
- data/lib/validatable-ext.rb +28 -0
- data/lib/validations/validates_inclusion_of.rb +22 -0
- data/sage_pay.gemspec +30 -5
- data/spec/integration/sage_pay/server_spec.rb +57 -0
- data/spec/sage_pay/server/address_spec.rb +119 -0
- data/spec/sage_pay/server/signature_verification_details_spec.rb +26 -0
- data/spec/sage_pay/server/transaction_code_spec.rb +15 -0
- data/spec/sage_pay/server/transaction_notification_response_spec.rb +75 -0
- data/spec/sage_pay/server/transaction_notification_spec.rb +142 -0
- data/spec/sage_pay/server/transaction_registration_response_spec.rb +231 -0
- data/spec/sage_pay/server/transaction_registration_spec.rb +619 -0
- data/spec/sage_pay/server_spec.rb +72 -0
- data/spec/sage_pay_spec.rb +7 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/factories.rb +58 -0
- data/spec/support/integration.rb +9 -0
- data/spec/support/validation_matchers.rb +71 -0
- metadata +79 -7
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'spec/rake/spectask'
|
3
4
|
require 'date'
|
4
5
|
|
5
6
|
#############################################################################
|
@@ -43,21 +44,13 @@ end
|
|
43
44
|
#
|
44
45
|
#############################################################################
|
45
46
|
|
46
|
-
task :default => :
|
47
|
+
task :default => :spec
|
47
48
|
|
48
|
-
|
49
|
-
Rake::TestTask.new(:test) do |test|
|
50
|
-
test.libs << 'lib' << 'test'
|
51
|
-
test.pattern = 'test/**/test_*.rb'
|
52
|
-
test.verbose = true
|
53
|
-
end
|
49
|
+
Spec::Rake::SpecTask.new
|
54
50
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
sh "rm -fr coverage"
|
59
|
-
sh "rcov test/test_*.rb"
|
60
|
-
sh "open coverage/index.html"
|
51
|
+
Spec::Rake::SpecTask.new(:coverage) do |coverage|
|
52
|
+
coverage.rcov = true
|
53
|
+
coverage.rcov_opts << '--exclude' << '\.rvm,spec'
|
61
54
|
end
|
62
55
|
|
63
56
|
require 'rake/rdoctask'
|
@@ -70,7 +63,7 @@ end
|
|
70
63
|
|
71
64
|
desc "Open an irb session preloaded with this library"
|
72
65
|
task :console do
|
73
|
-
sh "irb -rubygems -r ./lib/#{name}.rb"
|
66
|
+
sh "irb -rubygems -Ilib -r ./lib/#{name}.rb -r spec/support/factories.rb"
|
74
67
|
end
|
75
68
|
|
76
69
|
#############################################################################
|
@@ -87,6 +80,7 @@ end
|
|
87
80
|
#
|
88
81
|
#############################################################################
|
89
82
|
|
83
|
+
desc "Release the new version of the gem into the wild"
|
90
84
|
task :release => :build do
|
91
85
|
unless `git branch` =~ /^\* master$/
|
92
86
|
puts "You must be on the master branch to release!"
|
@@ -99,13 +93,14 @@ task :release => :build do
|
|
99
93
|
sh "gem push pkg/#{name}-#{version}.gem"
|
100
94
|
end
|
101
95
|
|
96
|
+
desc "Build the gem"
|
102
97
|
task :build => :gemspec do
|
103
98
|
sh "mkdir -p pkg"
|
104
99
|
sh "gem build #{gemspec_file}"
|
105
100
|
sh "mv #{gem_file} pkg"
|
106
101
|
end
|
107
102
|
|
108
|
-
task :gemspec
|
103
|
+
task :gemspec do
|
109
104
|
# read spec file and split out manifest section
|
110
105
|
spec = File.read(gemspec_file)
|
111
106
|
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
@@ -132,15 +127,3 @@ task :gemspec => :validate do
|
|
132
127
|
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
133
128
|
puts "Updated #{gemspec_file}"
|
134
129
|
end
|
135
|
-
|
136
|
-
task :validate do
|
137
|
-
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
138
|
-
unless libfiles.empty?
|
139
|
-
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
140
|
-
exit!
|
141
|
-
end
|
142
|
-
unless Dir['VERSION*'].empty?
|
143
|
-
puts "A `VERSION` file at root level violates Gem best practices."
|
144
|
-
exit!
|
145
|
-
end
|
146
|
-
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class Address
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_accessor :us_states, :iso_3166_country_codes
|
8
|
+
end
|
9
|
+
self.us_states = ["AK", "AL", "AR", "AS", "AZ", "CA", "CO", "CT", "DC", "DE", "FL", "FM", "GA", "GU", "HI", "IA", "ID", "IL", "IN", "KS", "KY", "LA", "MA", "MD", "ME", "MH", "MI", "MN", "MO", "MS", "MT", "NC", "ND", "NE", "NH", "NJ", "NM", "NV", "NY", "OH", "OK", "OR", "PA", "PR", "PW", "RI", "SC", "SD", "TN", "TX", "UT", "VA", "VI", "VT", "WA", "WI", "WV", "WY"]
|
10
|
+
self.iso_3166_country_codes = ["AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BA", "BW", "BV", "BR", "IO", "BN", "BG", "BF", "BI", "KH", "CM", "CA", "CV", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CG", "CD", "CK", "CR", "CI", "HR", "CU", "CY", "CZ", "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", "ER", "EE", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL", "IT", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KP", "KR", "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "AN", "NC", "NZ", "NI", "NE", "NG", "NU", "NF", "MP", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "SI", "SB", "SO", "ZA", "GS", "ES", "LK", "SD", "SR", "SJ", "SZ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM", "UY", "UZ", "VU", "VE", "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW"]
|
11
|
+
|
12
|
+
attr_accessor :first_names, :surname, :address_1, :address_2, :city,
|
13
|
+
:post_code, :country, :state, :phone
|
14
|
+
|
15
|
+
validates_presence_of :first_names, :surname, :address_1, :city, :post_code, :country
|
16
|
+
|
17
|
+
# FIXME: This regexp isn't correctly matching accented characters. I
|
18
|
+
# think it's a Ruby version issue so I'm punting for the moment.
|
19
|
+
validates_format_of :first_names, :surname, :with => /^[[:alpha:] \\\/&'\.\-]*$/
|
20
|
+
validates_format_of :address_1, :address_2, :city, :with => /^[[:alnum:][:space:]\+\\\/&'\.:,\(\)\-]*$/
|
21
|
+
validates_format_of :post_code, :with => /^[[:alnum:] -]*$/
|
22
|
+
validates_format_of :phone, :with => /^[[:alnum:] \+\(\)-]*$/
|
23
|
+
|
24
|
+
validates_length_of :first_names, :surname, :maximum => 20
|
25
|
+
validates_length_of :address_1, :address_2, :maximum => 100
|
26
|
+
validates_length_of :city, :maximum => 40
|
27
|
+
validates_length_of :post_code, :maximum => 10
|
28
|
+
validates_length_of :phone, :maximum => 20
|
29
|
+
|
30
|
+
# While the spec specifies the lengths of these columns, we're
|
31
|
+
# validating that they're included in our list, and our list only
|
32
|
+
# contains two-character strings, so this validation has no real win.
|
33
|
+
# validates_length_of :country, :state, :maximum => 2
|
34
|
+
|
35
|
+
validates_inclusion_of :state, :in => us_states, :allow_blank => true, :message => "is not a US state"
|
36
|
+
validates_inclusion_of :country, :in => iso_3166_country_codes, :allow_blank => true, :message => "is not an ISO3166-1 country code"
|
37
|
+
|
38
|
+
def initialize(attributes = {})
|
39
|
+
attributes.each do |k, v|
|
40
|
+
send("#{k}=", v)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class SignatureVerificationDetails
|
4
|
+
attr_reader :vps_tx_id, :vendor_tx_code, :vendor, :security_key
|
5
|
+
|
6
|
+
def initialize(transaction_registration, transaction_registration_response)
|
7
|
+
@vendor_tx_code = transaction_registration.vendor_tx_code
|
8
|
+
@vendor = transaction_registration.vendor
|
9
|
+
@vps_tx_id = transaction_registration_response.vps_tx_id
|
10
|
+
@security_key = transaction_registration_response.security_key
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class TransactionNotification
|
4
|
+
attr_reader :vps_protocol, :status, :status_detail, :tx_auth_no,
|
5
|
+
:avs_cv2, :address_result, :post_code_result, :cv2_result, :gift_aid,
|
6
|
+
:threed_secure_status, :cavv, :address_status, :payer_status,
|
7
|
+
:card_type, :last_4_digits, :vps_signature
|
8
|
+
|
9
|
+
def self.from_params(params, signature_verification_details = nil)
|
10
|
+
key_converter = {
|
11
|
+
"VPSProtocol" => :vps_protocol,
|
12
|
+
"Status" => :status,
|
13
|
+
"StatusDetail" => :status_detail,
|
14
|
+
"TxAuthNo" => :tx_auth_no,
|
15
|
+
"AVSCV2" => :avs_cv2,
|
16
|
+
"AddressResult" => :address_result,
|
17
|
+
"PostCodeResult" => :post_code_result,
|
18
|
+
"CV2Result" => :cv2_result,
|
19
|
+
"GiftAid" => :gift_aid,
|
20
|
+
"3DSecureStatus" => :threed_secure_status,
|
21
|
+
"CAVV" => :cavv,
|
22
|
+
"AddressStatus" => :address_status,
|
23
|
+
"PayerStatus" => :payer_status,
|
24
|
+
"CardType" => :card_type,
|
25
|
+
"Last4Digits" => :last_4_digits,
|
26
|
+
"VPSSignature" => :vps_signature
|
27
|
+
}
|
28
|
+
|
29
|
+
match_converter = {
|
30
|
+
"NOTPROVIDED" => :not_provided,
|
31
|
+
"NOTCHECKED" => :not_checked,
|
32
|
+
"MATCHED" => :matched,
|
33
|
+
"NOTMATCHED" => :not_matched
|
34
|
+
}
|
35
|
+
|
36
|
+
true_false_converter = {
|
37
|
+
"0" => false,
|
38
|
+
"1" => true
|
39
|
+
}
|
40
|
+
|
41
|
+
value_converter = {
|
42
|
+
:status => {
|
43
|
+
"OK" => :ok,
|
44
|
+
"NOTAUTHED" => :not_authed,
|
45
|
+
"ABORT" => :abort,
|
46
|
+
"REJECTED" => :rejected,
|
47
|
+
"AUTHENTICATED" => :authenticated,
|
48
|
+
"REGISTERED" => :registered,
|
49
|
+
"ERROR" => :error
|
50
|
+
},
|
51
|
+
:avs_cv2 => {
|
52
|
+
"ALL MATCH" => :all_match,
|
53
|
+
"SECURITY CODE MATCH ONLY" => :security_code_match_only,
|
54
|
+
"ADDRESS MATCH ONLY" => :address_match_only,
|
55
|
+
"NO DATA MATCHES" => :no_data_matches,
|
56
|
+
"DATA NOT CHECKED" => :data_not_checked
|
57
|
+
},
|
58
|
+
:address_result => match_converter,
|
59
|
+
:post_code_result => match_converter,
|
60
|
+
:cv2_result => match_converter,
|
61
|
+
:gift_aid => true_false_converter,
|
62
|
+
:threed_secure_status => {
|
63
|
+
"OK" => :ok,
|
64
|
+
"NOTCHECKED" => :not_checked,
|
65
|
+
"NOTAVAILABLE" => :not_available,
|
66
|
+
"NOTAUTHED" => :not_authed,
|
67
|
+
"INCOMPLETE" => :incomplete,
|
68
|
+
"ERROR" => :error
|
69
|
+
},
|
70
|
+
:address_status => {
|
71
|
+
"NONE" => :none,
|
72
|
+
"CONFIRMED" => :confirmed,
|
73
|
+
"UNCONFIRMED" => :unconfirmed
|
74
|
+
},
|
75
|
+
:payer_status => {
|
76
|
+
"VERIFIED" => :verified,
|
77
|
+
"UNVERIFIED" => :unverified
|
78
|
+
},
|
79
|
+
:card_type => {
|
80
|
+
"VISA" => :visa,
|
81
|
+
"MC" => :mastercard,
|
82
|
+
"DELTA" => :visa_delta,
|
83
|
+
"SOLO" => :solo,
|
84
|
+
"MAESTRO" => :maestro,
|
85
|
+
"UKE" => :visa_electron,
|
86
|
+
"AMEX" => :american_express,
|
87
|
+
"DC" => :diners,
|
88
|
+
"JCB" => :jcb,
|
89
|
+
"LASER" => :laser,
|
90
|
+
"PAYPAL" => :paypal
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
attributes = {}
|
95
|
+
params.each do |key, value|
|
96
|
+
unless value.nil?
|
97
|
+
converted_key = key_converter[key]
|
98
|
+
converted_value = value_converter[converted_key].nil? ? value : value_converter[converted_key][value]
|
99
|
+
|
100
|
+
attributes[converted_key] = converted_value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
unless signature_verification_details.nil?
|
105
|
+
# We need to calculate the VPS signature from the values passed in as
|
106
|
+
# additional params from the original registration and notification.
|
107
|
+
fields_used_in_signature = [
|
108
|
+
signature_verification_details.vps_tx_id,
|
109
|
+
signature_verification_details.vendor_tx_code,
|
110
|
+
params["Status"],
|
111
|
+
params["TxAuthNo"],
|
112
|
+
signature_verification_details.vendor,
|
113
|
+
params["AVSCV2"],
|
114
|
+
signature_verification_details.security_key,
|
115
|
+
params["AddressResult"],
|
116
|
+
params["PostCodeResult"],
|
117
|
+
params["CV2Result"],
|
118
|
+
params["GiftAid"],
|
119
|
+
params["3DSecureStatus"],
|
120
|
+
params["CAVV"],
|
121
|
+
params["AddressStatus"],
|
122
|
+
params["PayerStatus"],
|
123
|
+
params["CardType"],
|
124
|
+
params["Last4Digits"]
|
125
|
+
]
|
126
|
+
attributes[:calculated_hash] = MD5.md5(fields_used_in_signature.join).to_s.upcase
|
127
|
+
end
|
128
|
+
|
129
|
+
new(attributes)
|
130
|
+
end
|
131
|
+
|
132
|
+
def initialize(attributes = {})
|
133
|
+
attributes.each do |k, v|
|
134
|
+
# We're only providing readers, not writers, so we have to directly
|
135
|
+
# set the instance variable.
|
136
|
+
instance_variable_set("@#{k}", v)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def ok?
|
141
|
+
status == :ok
|
142
|
+
end
|
143
|
+
|
144
|
+
def avs_cv2_matched?
|
145
|
+
avs_cv2 == :all_match
|
146
|
+
end
|
147
|
+
|
148
|
+
def address_matched?
|
149
|
+
address_result == :matched
|
150
|
+
end
|
151
|
+
|
152
|
+
def post_code_matched?
|
153
|
+
post_code_result == :matched
|
154
|
+
end
|
155
|
+
|
156
|
+
def cv2_matched?
|
157
|
+
cv2_result == :matched
|
158
|
+
end
|
159
|
+
|
160
|
+
def threed_secure_status_ok?
|
161
|
+
threed_secure_status == :ok
|
162
|
+
end
|
163
|
+
|
164
|
+
def valid_signature?
|
165
|
+
@calculated_hash == vps_signature
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class TransactionNotificationResponse
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
attr_accessor :status, :status_detail, :redirect_url
|
7
|
+
|
8
|
+
validates_presence_of :status, :redirect_url
|
9
|
+
validates_presence_of :status_detail, :if => lambda { |response| !response.ok? }
|
10
|
+
|
11
|
+
validates_length_of :redirect_url, :maximum => 255
|
12
|
+
validates_length_of :status_detail, :maximum => 255
|
13
|
+
|
14
|
+
validates_inclusion_of :status, :allow_blank => true, :in => [ :ok, :invalid, :error ]
|
15
|
+
|
16
|
+
def initialize(attributes = {})
|
17
|
+
attributes.each do |k, v|
|
18
|
+
send("#{k}=", v)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def ok?
|
23
|
+
status == :ok
|
24
|
+
end
|
25
|
+
|
26
|
+
def response
|
27
|
+
response_params.map do |tuple|
|
28
|
+
key, value = tuple
|
29
|
+
"#{key}=#{value}"
|
30
|
+
end.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def response_params
|
35
|
+
raise ArgumentError, "Invalid transaction registration options (see errors hash for details)" unless valid?
|
36
|
+
|
37
|
+
# Mandatory parameters that we've already validated are present. Note
|
38
|
+
# that the order of parameters is important (Status must be first!) so
|
39
|
+
# we're using a list of lists this time around...
|
40
|
+
params = [
|
41
|
+
["Status", status.to_s.upcase],
|
42
|
+
["RedirectURL", redirect_url]
|
43
|
+
]
|
44
|
+
|
45
|
+
# Optional parameters that are only inserted if they are present
|
46
|
+
params << ['StatusDetail', status_detail] if present?(status_detail)
|
47
|
+
|
48
|
+
# And return the completed hash
|
49
|
+
params
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def present?(value)
|
54
|
+
!blank?(value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def blank?(value)
|
58
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class TransactionRegistration
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
attr_reader :vps_protocol
|
7
|
+
attr_accessor :mode, :tx_type, :vendor, :vendor_tx_code,
|
8
|
+
:amount, :currency, :description, :notification_url, :billing_address,
|
9
|
+
:delivery_address, :customer_email, :basket, :allow_gift_aid,
|
10
|
+
:apply_avs_cv2, :apply_3d_secure, :profile, :billing_agreement,
|
11
|
+
:account_type
|
12
|
+
|
13
|
+
validates_presence_of :mode, :vps_protocol, :tx_type, :vendor,
|
14
|
+
:vendor_tx_code, :amount, :currency, :description, :notification_url,
|
15
|
+
:billing_address, :delivery_address
|
16
|
+
|
17
|
+
validates_length_of :vps_protocol, :is => 4
|
18
|
+
validates_length_of :vendor, :maximum => 15
|
19
|
+
validates_length_of :vendor_tx_code, :maximum => 40
|
20
|
+
validates_length_of :currency, :is => 3
|
21
|
+
validates_length_of :description, :maximum => 100
|
22
|
+
validates_length_of :notification_url, :maximum => 255
|
23
|
+
validates_length_of :customer_email, :maximum => 255
|
24
|
+
validates_length_of :basket, :maximum => 7_500
|
25
|
+
|
26
|
+
validates_inclusion_of :mode, :allow_blank => true, :in => [ :simulator, :test, :live ]
|
27
|
+
validates_inclusion_of :tx_type, :allow_blank => true, :in => [ :payment, :deferred, :authenticate ]
|
28
|
+
validates_inclusion_of :allow_gift_aid, :allow_blank => true, :in => [ true, false ]
|
29
|
+
validates_inclusion_of :apply_avs_cv2, :allow_blank => true, :in => (0..3).to_a
|
30
|
+
validates_inclusion_of :apply_3d_secure, :allow_blank => true, :in => (0..3).to_a
|
31
|
+
validates_inclusion_of :profile, :allow_blank => true, :in => [:normal, :low]
|
32
|
+
validates_inclusion_of :billing_agreement, :allow_blank => true, :in => [true, false]
|
33
|
+
validates_inclusion_of :account_type, :allow_blank => true, :in => [:ecommerce, :continuous_authority, :mail_order]
|
34
|
+
|
35
|
+
validates_true_for :amount, :key => :amount_minimum_value, :logic => lambda { amount.nil? || amount >= BigDecimal.new("0.01") }, :message => "is less than the minimum value (0.01)"
|
36
|
+
validates_true_for :amount, :key => :amount_maximum_value, :logic => lambda { amount.nil? || amount <= BigDecimal.new("100000") }, :message => "is greater than the maximum value (100,000.00)"
|
37
|
+
|
38
|
+
def initialize(attributes = {})
|
39
|
+
# Effectively hard coded for now
|
40
|
+
@vps_protocol = "2.23"
|
41
|
+
|
42
|
+
attributes.each do |k, v|
|
43
|
+
send("#{k}=", v)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def register!
|
48
|
+
if @response.nil? || (@vendor_tx_code_sent != vendor_tx_code)
|
49
|
+
@vendor_tx_code_sent = vendor_tx_code
|
50
|
+
@response = handle_response(post)
|
51
|
+
else
|
52
|
+
raise RuntimeError, "This vendor transaction code has already been registered"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def signature_verification_details
|
57
|
+
if @response.nil?
|
58
|
+
raise RuntimeError, "Transaction not yet registered"
|
59
|
+
elsif @response.failed?
|
60
|
+
raise RuntimeError, "Transaction registration failed"
|
61
|
+
else
|
62
|
+
SignatureVerificationDetails.new(self, @response)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def url
|
67
|
+
case mode
|
68
|
+
when :simulator
|
69
|
+
"https://test.sagepay.com/simulator/VSPServerGateway.asp?Service=VendorRegisterTx"
|
70
|
+
when :test
|
71
|
+
"https://test.sagepay.com/gateway/service/vspserver-register.vsp"
|
72
|
+
when :live
|
73
|
+
"https://live.sagepay.com/gateway/service/vspserver-register.vsp"
|
74
|
+
else
|
75
|
+
raise ArgumentError, "Invalid transaction mode"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def post_params
|
80
|
+
raise ArgumentError, "Invalid transaction registration options (see errors hash for details)" unless valid?
|
81
|
+
|
82
|
+
# Mandatory parameters that we've already validated are present
|
83
|
+
params = {
|
84
|
+
"VPSProtocol" => vps_protocol,
|
85
|
+
"TxType" => tx_type.to_s.upcase,
|
86
|
+
"Vendor" => vendor,
|
87
|
+
"VendorTxCode" => vendor_tx_code,
|
88
|
+
"Amount" => ("%.2f" % amount),
|
89
|
+
"Currency" => currency.upcase,
|
90
|
+
"Description" => description,
|
91
|
+
"NotificationURL" => notification_url,
|
92
|
+
"BillingSurname" => billing_address.surname,
|
93
|
+
"BillingFirstnames" => billing_address.first_names,
|
94
|
+
"BillingAddress1" => billing_address.address_1,
|
95
|
+
"BillingCity" => billing_address.city,
|
96
|
+
"BillingPostCode" => billing_address.post_code,
|
97
|
+
"BillingCountry" => billing_address.country,
|
98
|
+
"DeliverySurname" => delivery_address.surname,
|
99
|
+
"DeliveryFirstnames" => delivery_address.first_names,
|
100
|
+
"DeliveryAddress1" => delivery_address.address_1,
|
101
|
+
"DeliveryCity" => delivery_address.city,
|
102
|
+
"DeliveryPostCode" => delivery_address.post_code,
|
103
|
+
"DeliveryCountry" => delivery_address.country,
|
104
|
+
}
|
105
|
+
|
106
|
+
# Optional parameters that are only inserted if they are present
|
107
|
+
params['BillingAddress2'] = billing_address.address_2 if present?(billing_address.address_2)
|
108
|
+
params['BillingState'] = billing_address.state if present?(billing_address.state)
|
109
|
+
params['BillingPhone'] = billing_address.phone if present?(billing_address.phone)
|
110
|
+
params['DeliveryAddress2'] = delivery_address.address_2 if present?(delivery_address.address_2)
|
111
|
+
params['DeliveryState'] = delivery_address.state if present?(delivery_address.state)
|
112
|
+
params['DeliveryPhone'] = delivery_address.phone if present?(delivery_address.phone)
|
113
|
+
params['CustomerEmail'] = customer_email if present?(customer_email)
|
114
|
+
params['Basket'] = basket if present?(basket)
|
115
|
+
params['AllowGiftAid'] = allow_gift_aid ? "1" : "0" if present?(allow_gift_aid)
|
116
|
+
params['ApplyAVSCV2'] = apply_avs_cv2.to_s if present?(apply_avs_cv2)
|
117
|
+
params['Apply3DSecure'] = apply_3d_secure.to_s if present?(apply_3d_secure)
|
118
|
+
params['Profile'] = profile.to_s.upcase if present?(profile)
|
119
|
+
params['BillingAgreement'] = billing_agreement ? "1" : "0" if present?(billing_agreement)
|
120
|
+
params['AccountType'] = account_type_param if present?(account_type)
|
121
|
+
|
122
|
+
# And return the completed hash
|
123
|
+
params
|
124
|
+
end
|
125
|
+
|
126
|
+
def amount=(value)
|
127
|
+
@amount = blank?(value) ? nil : BigDecimal.new(value.to_s)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def post
|
133
|
+
parsed_uri = URI.parse(url)
|
134
|
+
request = Net::HTTP::Post.new(parsed_uri.request_uri)
|
135
|
+
request.form_data = post_params
|
136
|
+
|
137
|
+
http = Net::HTTP.new(parsed_uri.host, parsed_uri.port)
|
138
|
+
http.use_ssl = true if parsed_uri.scheme == "https"
|
139
|
+
http.start { |http|
|
140
|
+
http.request(request)
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def handle_response(response)
|
145
|
+
case response.code.to_i
|
146
|
+
when 200
|
147
|
+
TransactionRegistrationResponse.from_response_body(response.body)
|
148
|
+
else
|
149
|
+
# FIXME: custom error response would be nice.
|
150
|
+
raise RuntimeError, "I guess SagePay doesn't like us today."
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def account_type_param
|
155
|
+
case account_type
|
156
|
+
when :ecommerce
|
157
|
+
'E'
|
158
|
+
when :continuous_authority
|
159
|
+
'C'
|
160
|
+
when :mail_order
|
161
|
+
'M'
|
162
|
+
else
|
163
|
+
raise ArgumentError, "account type is an invalid value: #{account_type}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def present?(value)
|
168
|
+
!blank?(value)
|
169
|
+
end
|
170
|
+
|
171
|
+
def blank?(value)
|
172
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module SagePay
|
2
|
+
module Server
|
3
|
+
class TransactionRegistrationResponse
|
4
|
+
attr_reader :vps_protocol, :status, :status_detail
|
5
|
+
|
6
|
+
def self.from_response_body(response_body)
|
7
|
+
key_converter = {
|
8
|
+
"VPSProtocol" => :vps_protocol,
|
9
|
+
"Status" => :status,
|
10
|
+
"StatusDetail" => :status_detail,
|
11
|
+
"VPSTxId" => :vps_tx_id,
|
12
|
+
"SecurityKey" => :security_key,
|
13
|
+
"NextURL" => :next_url
|
14
|
+
}
|
15
|
+
|
16
|
+
value_converter = {
|
17
|
+
:status => {
|
18
|
+
"OK" => :ok,
|
19
|
+
"MALFORMED" => :malformed,
|
20
|
+
"INVALID" => :invalid,
|
21
|
+
"ERROR" => :error
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
attributes = {}
|
26
|
+
response_body.each_line do |line|
|
27
|
+
key, value = line.split('=', 2)
|
28
|
+
unless key.nil? || value.nil?
|
29
|
+
value = value.chomp
|
30
|
+
|
31
|
+
converted_key = key_converter[key]
|
32
|
+
converted_value = value_converter[converted_key].nil? ? value : value_converter[converted_key][value]
|
33
|
+
|
34
|
+
attributes[converted_key] = converted_value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
new(attributes)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(attributes = {})
|
42
|
+
attributes.each do |k, v|
|
43
|
+
# We're only providing readers, not writers, so we have to directly
|
44
|
+
# set the instance variable.
|
45
|
+
instance_variable_set("@#{k}", v)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def ok?
|
50
|
+
status == :ok
|
51
|
+
end
|
52
|
+
|
53
|
+
def failed?
|
54
|
+
!ok?
|
55
|
+
end
|
56
|
+
|
57
|
+
def invalid?
|
58
|
+
status == :invalid
|
59
|
+
end
|
60
|
+
|
61
|
+
def malformed?
|
62
|
+
status == :malformed
|
63
|
+
end
|
64
|
+
|
65
|
+
def error?
|
66
|
+
status == :error
|
67
|
+
end
|
68
|
+
|
69
|
+
def vps_tx_id
|
70
|
+
if ok?
|
71
|
+
@vps_tx_id
|
72
|
+
else
|
73
|
+
raise RuntimeError, "Unable to retrieve the transaction id as the status was not OK."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def security_key
|
78
|
+
if ok?
|
79
|
+
@security_key
|
80
|
+
else
|
81
|
+
raise RuntimeError, "Unable to retrieve the security key as the status was not OK."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_url
|
86
|
+
if ok?
|
87
|
+
@next_url
|
88
|
+
else
|
89
|
+
raise RuntimeError, "Unable to retrieve the next URL as the status was not OK."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|