sage_party 0.0.1
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/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
|