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 +119 -0
- data/spec/sage_transaction_spec.rb +183 -0
- data/spec/spec_helper.rb +20 -0
- metadata +129 -0
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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|