sage_party 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sage_party.rb ADDED
@@ -0,0 +1,119 @@
1
+ require 'digest'
2
+ require 'active_support'
3
+ require 'party_resource'
4
+
5
+ PartyResource::Connector.add(:sage_party, {})
6
+
7
+ module SageParty
8
+ class Transaction
9
+ include PartyResource
10
+
11
+ party_connector :sage_party
12
+ URLS = {:simulator => 'https://test.sagepay.com/simulator/VSPServerGateway.asp?Service=VendorRegisterTx',
13
+ :test => 'https://test.sagepay.com/gateway/service/vspserver-register.vsp',
14
+ :live => 'https://live.sagepay.com/gateway/service/vspserver-register.vsp'}
15
+ ::SAGE_PAY_SERVER = :simulator unless Object.const_defined?('SAGE_PAY_SERVER')
16
+
17
+ connect :raw_register, :post => URLS[::SAGE_PAY_SERVER.to_sym], :as => :raw
18
+
19
+ %w{VPSProtocol StatusDetail VPSTxId SecurityKey NextURL
20
+ VPSTxId VendorTxCode Status TxAuthNo VendorName AVSCV2 SecurityKey
21
+ AddressResult PostCodeResult CV2Result GiftAid CAVV AddressStatus
22
+ PayerStatus CardType Last4Digits VPSSignature}.each do |name|
23
+ property name.underscore, :from => name
24
+ end
25
+ property :three_d_secure_status, :from => '3DSecureStatus'
26
+ property :id, :vendor_name
27
+
28
+
29
+ class << self
30
+ # Register a new transaction with SagePay
31
+ # @return [Transaction]
32
+ def register_tx(data)
33
+ response = raw_register(data)
34
+ hash = {}
35
+ response.split("\r\n").each do |line|
36
+ line = line.split("=", 2)
37
+ hash[line.first] = line.last
38
+ end
39
+ self.new(hash.merge({:id => data[:VendorTxCode], :vendor_name => data[:Vendor]}))
40
+ end
41
+
42
+ # Find a stored transaction
43
+ # @return [Transaction]
44
+ def find(vendor_id, sage_id)
45
+ transaction = get(vendor_id)
46
+ return missing_transaction if transaction.nil? || transaction.vps_tx_id != sage_id
47
+ transaction
48
+ end
49
+
50
+ protected
51
+ def get(vendor_id)
52
+ raise 'self.get method get needs to be defined'
53
+ end
54
+
55
+ private
56
+ def missing_transaction
57
+ self.new(:not_found => true)
58
+ end
59
+ end
60
+
61
+ # Return HTTP response to return to SagePay server
62
+ # @return [String]
63
+ def response
64
+ return format_response(:invalid, 'Transaction not found') unless exists?
65
+ return format_response(:invalid, 'Security check failed') unless signature_ok?
66
+ return format_response(:error, 'Sage Pay reported an error') if status == 'ERROR'
67
+ return format_response(:invalid, 'Unexpected status') if %w{AUTHENTICATED REGISTERED}.include?(status)
68
+ return format_response(:invalid, "Invalid status: #{status}") unless %w{OK NOTAUTHED ABORT REJECTED}.include?(status)
69
+ format_response(:ok)
70
+ end
71
+
72
+ # Test transaction equality
73
+ # @return [Boolean]
74
+ def ==(other)
75
+ properties_equal?(other) && self.exists? == other.exists?
76
+ end
77
+
78
+ # Detrmine if this transaction exists
79
+ def exists?
80
+ !@not_found
81
+ end
82
+
83
+ # Merge in transaction stage two data
84
+ # @return [Transaction] self
85
+ def merge!(data)
86
+ data = data.with_indifferent_access
87
+ data.delete(:SecurityKey)
88
+ populate_properties(data)
89
+ self
90
+ end
91
+
92
+ # Check if transaction data matches its signature
93
+ def signature_ok?
94
+ generate_md5 == vps_signature
95
+ end
96
+
97
+ protected
98
+ def initialize(params)
99
+ populate_properties(params)
100
+ @not_found = params[:not_found]
101
+ end
102
+
103
+ def notification_url
104
+ raise 'notification_url method needs to be defined'
105
+ end
106
+
107
+ private
108
+ def generate_md5
109
+ Digest::MD5.hexdigest("#{vps_tx_id}#{vendor_tx_code}#{status}#{tx_auth_no}#{vendor_name}#{avscv2}#{security_key}#{address_result}#{post_code_result}#{cv2_result}#{gift_aid}#{three_d_secure_status}#{cavv}#{address_status}#{payer_status}#{card_type}#{last4_digits}").upcase
110
+ end
111
+
112
+ def format_response(status, details=nil)
113
+ str = "Status=#{status.to_s.upcase}\r\nRedirectURL=#{notification_url}"
114
+ str += "\r\nStatusDetail=#{details}" unless details.nil?
115
+ str
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,183 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ def m(name)
4
+ v = mock(name)
5
+ instance_variable_set("@#{name}", v)
6
+ end
7
+
8
+ describe SageParty::Transaction do
9
+ describe 'registering a transaction' do
10
+ before do
11
+ SageParty::Transaction.stub!(:raw_register => '')
12
+ SageParty::Transaction.stub!(:tx_id => 'my_tx_id')
13
+ @basket = mock(:basket, :id => mock, :null_object => true)
14
+ @customer = mock(:customer, :null_object => true)
15
+ @data = {:VendorTxCode => m(:tx_code), :Vendor => m(:vendor)}
16
+ end
17
+
18
+ it 'registers with the data' do
19
+ SageParty::Transaction.should_receive(:raw_register).with(@data)
20
+ SageParty::Transaction.register_tx(@data)
21
+ end
22
+
23
+ it 'parses the response' do
24
+ SageParty::Transaction.stub!(:raw_register => "VPSProtocol=2.23\r\nStatus=OK\r\nStatusDetail=Server transaction registered successfully.\r\nVPSTxId={F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}\r\nSecurityKey=YK4N4LO9PT\r\nNextURL=https://test.sagepay.com/Simulator/VSPServerPaymentPage.asp?SageTransactionID={F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}")
25
+ result = SageParty::Transaction.new({:vps_protocol => '2.23', :status => 'OK', :status_detail => 'Server transaction registered successfully.', :vps_tx_id => '{F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}', :security_key => 'YK4N4LO9PT', :next_url => 'https://test.sagepay.com/Simulator/VSPServerPaymentPage.asp?SageTransactionID={F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}', :id => @tx_code, :vendor_name => @vendor, :basket_id => @basket.id})
26
+ SageParty::Transaction.register_tx(@data).should == result
27
+ end
28
+ end
29
+
30
+ describe 'find' do
31
+ it 'looks for the transaction' do
32
+ SageParty::Transaction.should_receive(:get).with('foo')
33
+ SageParty::Transaction.find('foo', 'bar')
34
+ end
35
+
36
+ context 'transaction can be found' do
37
+ it 'returns the transaction' do
38
+ transaction = mock(:transaction, :vps_tx_id => 'bar')
39
+ SageParty::Transaction.stub!(:get => transaction)
40
+ SageParty::Transaction.find('foo', 'bar').should == transaction
41
+ end
42
+ end
43
+
44
+ context 'transaction cannot be found' do
45
+ it 'returns a "null" transaction' do
46
+ SageParty::Transaction.stub!(:get => nil)
47
+ SageParty::Transaction.find('foo', 'bar').should_not be_exists
48
+ end
49
+ end
50
+
51
+ context 'transaction VPSTxId mismatch' do
52
+ it 'returns a "null" transaction' do
53
+ SageParty::Transaction.stub!(:get => mock(:transaction, :vps_tx_id => mock))
54
+ SageParty::Transaction.find('foo', 'bar').should_not be_exists
55
+ end
56
+ end
57
+ end
58
+
59
+ describe 'merge!' do
60
+ before do
61
+ @transaction = SageParty::Transaction.new('CardType' => m(:original_card_type), 'Status' => m(:status), 'SecurityKey' => m(:original_security_key))
62
+ @transaction.merge!('GiftAid' => m(:gift_aid), 'CardType' => m(:card_type), 'SecurityKey' => m(:security_key))
63
+ end
64
+
65
+ it 'repopulates existing data' do
66
+ @transaction.card_type.should == @card_type
67
+ end
68
+
69
+ it 'populates new data' do
70
+ @transaction.gift_aid.should == @gift_aid
71
+ end
72
+
73
+ it 'leaves unchanged data unchaged' do
74
+ @transaction.status.should == @status
75
+ end
76
+
77
+ it 'DOES NOT repopulate the security key' do
78
+ @transaction.security_key.should == @original_security_key
79
+ end
80
+ end
81
+
82
+ describe 'response' do
83
+ before do
84
+ @transaction = SageParty::Transaction.new({:id => mock(:id)})
85
+ @transaction.stub!(:notification_url => 'notify_url')
86
+ @transaction.stub!(:signature_ok? => true)
87
+ end
88
+
89
+ def set_status(status)
90
+ @transaction.merge!('Status' => status)
91
+ end
92
+
93
+ def check_response(*args)
94
+ @transaction.response.should == @transaction.send(:format_response, *args)
95
+ end
96
+
97
+ context 'transaction not found' do
98
+ it 'returns invalid' do
99
+ @transaction.stub!(:exists? => false)
100
+ check_response(:invalid, 'Transaction not found')
101
+ end
102
+ end
103
+
104
+ context 'with incorrect signature' do
105
+ before do
106
+ @transaction.stub!(:signature_ok? => false)
107
+ end
108
+
109
+ it 'returns invalid' do
110
+ check_response(:invalid, 'Security check failed')
111
+ end
112
+ end
113
+
114
+ context 'when status == ERROR' do
115
+ before do
116
+ set_status('ERROR')
117
+ end
118
+
119
+ it 'returns error' do
120
+ check_response(:error, 'Sage Pay reported an error')
121
+ end
122
+ end
123
+
124
+ context 'status is an unexpected value' do
125
+ it 'returns invalid when status is incorrect value' do
126
+ set_status('CUSTARD')
127
+ check_response(:invalid, 'Invalid status: CUSTARD')
128
+ end
129
+
130
+ it 'returns invalid when status is blank' do
131
+ set_status('')
132
+ check_response(:invalid, 'Invalid status: ')
133
+ end
134
+
135
+ it 'returns invalid when status is nil' do
136
+ set_status(nil)
137
+ check_response(:invalid, 'Invalid status: ')
138
+ end
139
+
140
+ it 'returns invalid/unexpected when status is AUTHENTICATED' do
141
+ set_status('AUTHENTICATED')
142
+ check_response(:invalid, 'Unexpected status')
143
+ end
144
+
145
+ it 'returns invalid/unexpected when status is REGISTERED' do
146
+ set_status('REGISTERED')
147
+ check_response(:invalid, 'Unexpected status')
148
+ end
149
+ end
150
+
151
+ context 'status is an expected value' do
152
+ %w{OK NOTAUTHED ABORT REJECTED}.each do |status|
153
+ it "returns ok when status is #{status}" do
154
+ set_status(status)
155
+ check_response(:ok)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe 'signature_ok?' do
162
+ before do
163
+ @transaction = SageParty::Transaction.new({:vps_protocol => '2.23', :status => 'OK', :status_detail => 'Server transaction registered successfully.', :vps_tx_id => '{F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}', :security_key => 'YK4N4LO9PT', :next_url => 'https://test.sagepay.com/Simulator/VSPServerPaymentPage.asp?SageTransactionID={F2A9E367-AC15-4F5F-AB4C-D74B5A0EE8CF}', :id => 'tx_code', :vendor_name => 'sage_key'})
164
+ end
165
+
166
+ it 'passes if the signature matches' do
167
+ @transaction.merge!(:vps_signature => 'DBCB54EB1128738F0C9E48600CBCF4DA')
168
+ @transaction.signature_ok?.should be_true
169
+ end
170
+
171
+ it 'fails if the signature does not match' do
172
+ @transaction.merge!(:vps_signature => 'CBCB54EB1128738F0C9E48600CBCF4DA')
173
+ @transaction.signature_ok?.should be_false
174
+ end
175
+
176
+ it 'fails if the signature is unset' do
177
+ @transaction.signature_ok?.should be_false
178
+ end
179
+ end
180
+
181
+ end
182
+
183
+
@@ -0,0 +1,20 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'sage_party'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+ require 'webmock/rspec'
8
+
9
+ include WebMock
10
+
11
+ module LetMock
12
+ def let_mock(name, options = {})
13
+ let(name) { mock(name, options) }
14
+ end
15
+ end
16
+
17
+ Spec::Runner.configure do |config|
18
+ config.extend(LetMock)
19
+ end
20
+
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sage_party
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Tristan Harris
13
+ - Steve Tooke
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-05-28 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: party_resource
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 0
31
+ - 2
32
+ version: 0.0.2
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: activesupport
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 2
44
+ - 3
45
+ - 5
46
+ version: 2.3.5
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 2
59
+ - 9
60
+ version: 1.2.9
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: yard
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ type: :development
74
+ version_requirements: *id004
75
+ - !ruby/object:Gem::Dependency
76
+ name: webmock
77
+ prerelease: false
78
+ requirement: &id005 !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ type: :development
86
+ version_requirements: *id005
87
+ description: sage_party is a simple interface to SagePay's Server service built on top of party_resource
88
+ email: dev+sage_party@edendevelopment.co.uk
89
+ executables: []
90
+
91
+ extensions: []
92
+
93
+ extra_rdoc_files: []
94
+
95
+ files:
96
+ - lib/sage_party.rb
97
+ has_rdoc: true
98
+ homepage: http://github.com/edendevelopment/sage_party.git
99
+ licenses: []
100
+
101
+ post_install_message:
102
+ rdoc_options:
103
+ - --charset=UTF-8
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project:
123
+ rubygems_version: 1.3.6
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: Simple interface to the SagePay Server service.
127
+ test_files:
128
+ - spec/sage_transaction_spec.rb
129
+ - spec/spec_helper.rb