sage_pay 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|