solon 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -2,142 +2,150 @@ h1. Solon - Sagepay VSP Server
2
2
 
3
3
  h2. Introduction
4
4
 
5
- Solon is a library for accessing the Sagepay VSP Server. Its patterns are loosely modeled on "ActiveMerchant":http://www.activemerchant.org/, for clarity (unfortunately AM doesn't support the VSP Server mode of operation). Solon was built for an application called "Woobius":https://www.woobius.com, but is released free of charge (and free of any warranties) to the community in the hope that it will be useful.
5
+ Solon is another library for interacting with the sagepay VSP server. It is largely the codebase from the seemingly defunct Peeves library with some modifications.
6
6
 
7
- Using the Sagepay VSP Server enables you to not have to host payment pages on your own site. You initiate a transaction, then forward the user to the page returned by Sagepay, and the rest is handled off-site. This has many benefits, in particular not needing to implement credit card validation pages yourself, not needing to handle credit card numbers directly (which means that you're automatically PCI-DSS compliant), and not needing to deal with 3D-secure yourself.
7
+ This library is not the cleanest out there and you may want to look at the other sagepay offerings before using this library.
8
8
 
9
- Sagepay is an UK payment gateway. You can find more information on their VPS Server service "here":http://techsupport.Sagepay.com/vspservercustom.asp.
10
9
 
11
- h2. Licence
10
+ h2. Installation (Rails 3)
12
11
 
13
- The initial version of Solon was written from scratch by Daniel Tenner. It is loosely based on ActiveMerchant's patterns, but all code is original, and is licenced under the "Apache Licence":http://www.oss-watch.ac.uk/resources/apache2.xml.
12
+ Add the following to your Gemfile
14
13
 
15
- As a user, this means you can effectively use Solon for whatever you want so long as you agree not to sue people about it.
14
+ <pre>
15
+ gem 'solon'
16
+ </pre>
16
17
 
17
- As a contributor, it means that you licence to Solon any contributions that you make as per the Apache licence.
18
+ <pre>
19
+ bundle
20
+ </pre>
18
21
 
19
- h2. Kicking the tyres
22
+ In each of your environments you can define the sagepay mode of operation accordingly.
20
23
 
21
- After setting up your IP address in the VSP Simulator, and obtaining a valid simulator vendor name, temporarily edit <code>simulate.rb</code> with your *vendor name* and run:
24
+ In config/environments/(development|test|production|[staging]).rb
22
25
 
23
26
  <pre>
24
- ruby simulate.rb
27
+ Solon::Config.vendor = [vendor_name]
28
+ Solon::Config.gateway_mode = :simulator | :test | :live
25
29
  </pre>
26
30
 
27
- Everything should work. If it doesn't work, try asking me (swombat) for help on freenode, channel #flails.
31
+ h2. Integration Guide
28
32
 
29
- Assuming everything worked fine, revert your change to simulate.rb.
33
+ Best plan is to generate yourself an controller for dealing with the integration.
30
34
 
31
- h2. Usage
32
35
 
33
- h3. Configuration
36
+ rails g controller sages
34
37
 
35
- In each environment file, you want to do something like this:
38
+ in routes.rb
36
39
 
37
- <pre>
38
- PEEVES_VENDOR = "woobius"
39
- PEEVES_GATEWAY_MODE = :simulator
40
- </pre>
40
+ resources :sages
41
41
 
42
- The gateway mode can be set to <code>:simulator</code>, <code>:test</code>, or <code>:live</code>.
43
42
 
44
- Then, in environment.rb, do:
43
+ Assumes an order has an identifier in order.identifier
45
44
 
46
45
  <pre>
47
- Solon::Config.vendor = PEEVES_VENDOR
48
- Solon::Config.gateway_mode = PEEVES_GATEWAY_MODE
49
- </pre>
46
+ require 'digest/sha1'
50
47
 
51
- h3. Making a request
48
+ before_create :set_identifier
52
49
 
53
- As per <code>simulate.rb</code>:
54
-
55
- <pre>
56
- transaction_reference = Solon::UniqueId.generate("TEST")
57
- customer_data = Solon::CustomerData.new(:surname => 'blah',
58
- :firstnames => 'blah',
59
- :address1 => 'blah',
60
- :address2 => 'blah',
61
- :city => 'blah',
62
- :post_code => 'blah',
63
- :country => 'gb',
64
- :email => 'customer@email.com'
65
- )
66
-
67
- # Payment registration
68
- payment_response = p.payment(Solon::Money.new(1000, "GBP"),
69
- {
70
- :transaction_reference => transaction_reference,
71
- :description => "Test Transaction",
72
- :notification_url => "http://callback.example.com/process_stuff",
73
- :customer_data => { :billing => customer_data,
74
- :delivery => customer_data,
75
- :email => customer_data.email }
76
- })
50
+ def set_identifier
51
+ self.identifier = Digest::SHA1.hexdigest(rand(1000).to_s + Time.now.to_s.split(//).sort_by {rand}.join)
52
+ end
77
53
  </pre>
78
54
 
79
- This will register a payment and return a url that you should forward the user to.
55
+ It is a good plan to create an order_transaction model to store order transactions locally for debugging.
80
56
 
81
- Once the user has made the payment on the Sagepay VSP Server pages, you will receive a callback at the URL you defined as <code>:notification_url</code>. You should call something like the following on the parameters that you receive back:
57
+ The first step is registering the payment.
82
58
 
83
59
  <pre>
84
- def record_response_params(params)
85
- returning SolonGateway.parse_notification(params) do |response|
86
- self.update_attributes(
87
- :last_status => response.status,
88
- :last_status_detail => response.last_status_detail,
89
- :vps_transaction_id => response.vps_transaction_id,
90
- :transaction_authorisation_number => response.transaction_authorisation_number,
91
- :status_3d_secure => response.status_3d_secure,
92
- :code_3d_secure => response.code_3d_secure
93
- )
94
- end
60
+ def create
61
+ @order.update_attribute(:transaction_reference, Solon::UniqueId.generate(@order.id.to_s))
62
+
63
+ p = SolonGateway.new
64
+ @response = p.payment Solon::Money.new(@order.amount.to_f, "GBP"),
65
+ {
66
+ :transaction_reference => @order.transaction_reference,
67
+ :description => "Description of payment",
68
+ :notification_url => callback_sage_url(@order.identifier),
69
+ :customer_data => @order.sage_customer_data
70
+ }
71
+ if @response.next_url
72
+ @order.transactions.create(:params => @response, :success => true, :message => "Purchase Transaction",
73
+ :amount => @order.amount.to_f, :reference => @order.transaction_reference)
74
+ @order.update_attributes(:security_key => @response.security_key, :user_location => 'away')
75
+ @order.send_pending_email
76
+ redirect_to @response.next_url
77
+ else
78
+ @order.transactions.create(:params => @response, :success => false, :message => "Purchase Transaction")
79
+ flash[:warning] = "Unable to make payment, please contact support."
80
+ redirect_to root_url
95
81
  end
82
+ end
96
83
  </pre>
97
84
 
98
- In response to this, you must send back a success message or the transaction will eventually be cancelled by Sagepay:
85
+ The next step is handling to callbacks from sage page.
99
86
 
100
87
  <pre>
101
- render :text => SolonGateway.response(SolonGateway::APPROVED, url, "Payment succeeded")
102
- </pre>
88
+ def callback
89
+ # Parse the callback
90
+ callback = SolonGateway.parse_callback(request.raw_post)
91
+
92
+
93
+ # Get the order from identifier
94
+ begin
95
+ @order = Order.find_by_identifier!(params[:id])
96
+ @order.sage_callback = callback
97
+ rescue ActiveRecord::RecordNotFound => msg
98
+ # Error if cannot find.
99
+ render :text => SolonGateway.response('INVALID', no_order_sage_url, 'Transaction complete'), :layout => false
100
+ return
101
+ end
102
+
103
+
104
+ # If the order has already been paid, render an OK response.
105
+ if @order.paid?
106
+ render_response(@order)
107
+ return
108
+ end
103
109
 
104
- Sagepay will then forward the user to <code>url</code>, where you can display a happy success page.
110
+ if @order.valid_callback?
111
+ # all is well
112
+ else
113
+ # error
114
+ end
105
115
 
106
- h3. Repeat transactions
116
+ render_response(@order)
117
+ end
118
+ </pre>
107
119
 
108
- Sagepay VSP Server wouldn't be worth much without repeat transactions. Here's an example of how to use them (this code sits on an Invoice class in Woobius):
120
+ Various responses tell the app different things about the order and you need to render a response in the right manner.
109
121
 
110
122
  <pre>
111
- def pay_from(previous_invoice)
112
- response = SolonGateway.new.repeat(Solon::Money.new(self.currency_amount, self.currency),
113
- {
114
- :transaction_reference => self.reference,
115
- :description => self.description,
116
- :related_transaction_reference => previous_invoice.reference,
117
- :related_vps_transaction_id => previous_invoice.vps_transaction_id,
118
- :related_security_key => previous_invoice.security_key,
119
- :related_transaction_authorisation_number => previous_invoice.transaction_authorisation_number
120
- })
121
- self.update_attributes(
122
- :last_status => response.status,
123
- :last_status_detail => response.status_detail,
124
- :vps_transaction_id => response.vps_transaction_id,
125
- :security_key => response.security_key
126
- )
127
- if response.approved?
128
- self.paid!
123
+ private
124
+ def render_response(order)
125
+ if order.valid_callback?
126
+ render :text => SolonGateway.response('OK', sage_url(order), 'Transaction complete'), :layout => false
127
+ elsif order.invalid_callback?
128
+ render :text => SolonGateway.response('INVALID', invalid_order_sage_url(order), 'Transaction complete'), :layout => false
129
+ elsif order.sage_callback.st(:NOTAUTHED)
130
+ render :text => SolonGateway.response('OK', notauthed_sage_url(order), 'Transaction complete'), :layout => false
131
+ elsif order.sage_callback.st(:ABORT)
132
+ render :text => SolonGateway.response('OK', abort_sage_url(order), 'Transaction complete'), :layout => false
133
+ elsif order.sage_callback.st(:REJECTED)
134
+ render :text => SolonGateway.response('OK', rejected_sage_url(order), 'Transaction complete'), :layout => false
135
+ elsif order.sage_callback.error?
136
+ render :text => SolonGateway.response('INVALID', invalid_sage_url(order), 'Transaction complete'), :layout => false
137
+ else
138
+ render :text => SolonGateway.response('ERROR', error_sage_url(order), 'Transaction complete'), :layout => false
129
139
  end
130
-
131
- response
132
140
  end
133
141
  </pre>
134
142
 
135
- h3. Keeping abreast of changes
136
143
 
137
- Things might change. If you pull the latest version of Solon and something doesn't work, check the changelog: "CHANGES.textile":http://github.com/swombat/peeves/tree/master/CHANGES.textile.
144
+ Valid and invalid callbacks not only check that the callback is approved (callback.approved?) but also check that the value of the order locally has not changed since the transaction began. This is a danger with offsite payment gateways.
145
+
146
+ The checking of whether a callback is valid is left to the app developer.
147
+
148
+
138
149
 
139
- h2. Contributing back
140
150
 
141
- Please use the fork functionality on github to make a fork and then push back your changes to the fork queue. I will probably accept most useful changes, but it might take me a few days before I get around to it!
142
151
 
143
- Thanks!
data/solon.gemspec CHANGED
@@ -4,7 +4,7 @@ $:.unshift lib unless $:.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "solon"
7
- s.version = "0.0.5"
7
+ s.version = "0.1.0"
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Alastair Brunton"]
10
10
  s.email = ["info@simplyexcited.co.uk"]
@@ -6,31 +6,31 @@ describe SolonGateway do
6
6
  @p = SolonGateway.new(:simulator)
7
7
  @p.debug = false
8
8
  end
9
-
9
+
10
10
  describe "sending a payment request" do
11
11
  before(:each) do
12
12
  customer_data = Solon::CustomerData.new(:surname => 'blah',
13
- :firstnames => 'blah',
14
- :address1 => 'blah',
15
- :address2 => 'blah',
16
- :city => 'blah',
17
- :post_code => 'blah',
18
- :country => 'gb'
19
- )
13
+ :firstnames => 'blah',
14
+ :address1 => 'blah',
15
+ :address2 => 'blah',
16
+ :city => 'blah',
17
+ :post_code => 'blah',
18
+ :country => 'gb'
19
+ )
20
20
 
21
21
  @response = @p.payment(Solon::Money.new(1000, "GBP"),
22
- {
23
- :transaction_reference => Solon::UniqueId.generate("TEST"),
24
- :description => "Test Transaction",
25
- :notification_url => "http://test.example.com",
26
- :customer_data => {:billing => customer_data, :delivery =>customer_data} ,
27
- })
22
+ {
23
+ :transaction_reference => Solon::UniqueId.generate("TEST"),
24
+ :description => "Test Transaction",
25
+ :notification_url => "http://test.example.com",
26
+ :customer_data => {:billing => customer_data, :delivery =>customer_data} ,
27
+ })
28
28
  end
29
-
29
+
30
30
  it "should return a SagepayResponse" do
31
31
  @response.is_a?(Solon::SagepayResponse).should be_true
32
32
  end
33
-
33
+
34
34
  it "should have a vps_transaction_id" do
35
35
  @response.vps_transaction_id.should_not be_nil
36
36
  end
@@ -38,7 +38,7 @@ describe SolonGateway do
38
38
  it "should have a security_key" do
39
39
  @response.security_key.should_not be_nil
40
40
  end
41
-
41
+
42
42
  it "should not have a transaction_authorisation_number" do
43
43
  @response.transaction_authorisation_number.should be_nil
44
44
  end
@@ -46,60 +46,75 @@ describe SolonGateway do
46
46
 
47
47
  # commented out
48
48
  # AUTHORISE can only be used after AUTHENTICATE, not PAYMENT
49
- # describe "sending an authenticate request" do
50
- # before(:each) do
51
- # pending
52
- # @transaction_reference = Solon::UniqueId.generate("TEST")
53
- # @response = @p.authenticate Solon::Money.new(1000, "GBP"),
54
- # {
55
- # :transaction_reference => @transaction_reference,
56
- # :description => "Test Transaction",
57
- # :notification_url => "http://test.example.com"
58
- # }
59
- # end
60
- #
61
- # it "should return a SagepayResponse" do
62
- # @response.is_a?(Solon::SagepayResponse).should be_true
63
- # end
64
- #
65
- # it "should have a vps_transaction_id" do
66
- # @response.vps_transaction_id.should_not be_nil
67
- # end
68
- #
69
- # it "should have a security_key" do
70
- # @response.security_key.should_not be_nil
71
- # end
72
- #
73
- # it "should not have a transaction_authorisation_number" do
74
- # @response.transaction_authorisation_number.should be_nil
75
- # end
76
- #
77
- # it "should be cancellable" do
78
- # @response2 = @p.cancel({
79
- # :transaction_reference => @transaction_reference,
80
- # :vps_transaction_id => @response.vps_transaction_id,
81
- # :security_key => @response.security_key
82
- # })
83
- # @response2.status.should == SolonGateway::APPROVED
84
- # end
85
- # end
49
+ describe "sending an authenticate request" do
50
+ before(:each) do
51
+ # pending
52
+ @transaction_reference = Solon::UniqueId.generate("TEST")
53
+ customer_data = Solon::CustomerData.new(:surname => 'blah',
54
+ :firstnames => 'blah',
55
+ :address1 => 'blah',
56
+ :address2 => 'blah',
57
+ :city => 'blah',
58
+ :post_code => 'blah',
59
+ :country => 'gb'
60
+ )
61
+ @response = @p.authenticate Solon::Money.new(1000, "GBP"),
62
+ {
63
+ :transaction_reference => @transaction_reference,
64
+ :description => "Test Transaction",
65
+ :notification_url => "http://test.example.com",
66
+ :customer_data => {:billing => customer_data, :delivery =>customer_data}
67
+ }
68
+ end
69
+
70
+ it "should return a SagepayResponse" do
71
+ @response.is_a?(Solon::SagepayResponse).should be_true
72
+ end
73
+
74
+ it "should have a vps_transaction_id" do
75
+ @response.vps_transaction_id.should_not be_nil
76
+ end
77
+
78
+ it "should have a security_key" do
79
+ @response.security_key.should_not be_nil
80
+ end
81
+
82
+ it "should not have a transaction_authorisation_number" do
83
+ @response.transaction_authorisation_number.should be_nil
84
+ end
85
+
86
+ # it "should be cancellable" do
87
+ # pending
88
+ # @response2 = @p.cancel({
89
+ # :transaction_reference => @transaction_reference,
90
+ # :vps_transaction_id => @response.vps_transaction_id,
91
+ # :security_key => @response.security_key,
92
+ # :related_vps_transaction_id => '12345'
93
+ # })
94
+ # puts @response2.inspect
95
+ # puts @response.transaction_authorisation_number
96
+ # puts @response.vps_transaction_id
97
+ # puts @response.inspect
98
+ # @response2.status.should == SolonGateway::APPROVED
99
+ # end
100
+ end
86
101
 
87
102
  describe "receiving a notification" do
88
103
  before(:each) do
89
104
  params = {
90
- "Status"=>"OK",
91
- "TxType"=>"PAYMENT",
92
- "VPSTxId"=>"{861A2DB0-E734-4DEB-8F8B-12C47B9ADF3E}",
93
- "VendorTxCode"=>"W-TEST-1227524828.86576-59414",
94
- "GiftAid"=>"0",
95
- "AVSCV2"=>"ALL MATCH",
96
- "TxAuthNo"=>"8661",
97
- "VPSProtocol"=>"2.22",
98
- "CAVV"=>"MNL2CYF4URE47IQNBI6DAH",
99
- "3DSecureStatus"=>"OK",
100
- "VPSSignature"=>"49A6FA9FE0631919D9B1E72ACE57584D",
101
- "CV2Result"=>"MATCHED",
102
- "PostCodeResult"=>"MATCHED",
105
+ "Status"=>"OK",
106
+ "TxType"=>"PAYMENT",
107
+ "VPSTxId"=>"{861A2DB0-E734-4DEB-8F8B-12C47B9ADF3E}",
108
+ "VendorTxCode"=>"W-TEST-1227524828.86576-59414",
109
+ "GiftAid"=>"0",
110
+ "AVSCV2"=>"ALL MATCH",
111
+ "TxAuthNo"=>"8661",
112
+ "VPSProtocol"=>"2.22",
113
+ "CAVV"=>"MNL2CYF4URE47IQNBI6DAH",
114
+ "3DSecureStatus"=>"OK",
115
+ "VPSSignature"=>"49A6FA9FE0631919D9B1E72ACE57584D",
116
+ "CV2Result"=>"MATCHED",
117
+ "PostCodeResult"=>"MATCHED",
103
118
  "AddressResult"=>"MATCHED"
104
119
  }
105
120
  @result = SolonGateway.parse_notification(params)
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  require 'rubygems'
2
- require 'activesupport'
2
+ require 'active_support'
3
+ require 'active_support/core_ext'
4
+
5
+ require File.dirname(__FILE__) + "/../lib/solon"
6
+
7
+
8
+ class Rails
9
+ def self.logger
10
+ Logger.new('/tmp/log.txt')
11
+ end
12
+ end
13
+
14
+ Solon::Config.vendor = '[add vendorname here for simulator]'
15
+ Solon::Config.gateway_mode = :simulator
3
16
 
4
- $LOAD_PATH << File.dirname(__FILE__) + "/../lib/"
5
17
 
6
- ActiveSupport::Dependencies.load_paths = $LOAD_PATH
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solon
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 5
10
- version: 0.0.5
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Alastair Brunton
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-08-19 00:00:00 +02:00
18
+ date: 2011-09-02 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies: []
21
21