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 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 => :test
47
+ task :default => :spec
47
48
 
48
- require 'rake/testtask'
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
- desc "Generate RCov test coverage and open in your browser"
56
- task :coverage do
57
- require 'rcov'
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 => :validate do
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,18 @@
1
+ module SagePay
2
+ module Server
3
+ class TransactionCode
4
+ def self.random
5
+ new.random
6
+ end
7
+
8
+ def random
9
+ uuid.generate
10
+ end
11
+
12
+ protected
13
+ def uuid
14
+ @uuid ||= UUID.new
15
+ end
16
+ end
17
+ end
18
+ 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