secure_trading 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'uri'
3
+ require 'net/https'
4
+ require 'xmlsimple'
5
+ require 'iconv'
6
+ require 'fileutils'
7
+
8
+ SECURETRADING_API_VERSION = '3.51'
9
+
10
+ require "secure_trading/util"
11
+ require "secure_trading/connection"
12
+ require "secure_trading/xpay_client"
13
+ require "secure_trading/request"
14
+ require "secure_trading/response"
15
+ require "secure_trading/data_object"
16
+
17
+ require 'secure_trading/requests/authorisation'
18
+ require 'secure_trading/requests/authorisation_with_tds'
19
+ require 'secure_trading/requests/continuous_authorisation'
20
+ require 'secure_trading/requests/authorisation_reversal'
21
+ require 'secure_trading/requests/transaction_query'
22
+ require 'secure_trading/requests/settlement'
23
+
24
+ require 'secure_trading/objects/customer'
25
+ require 'secure_trading/objects/order'
26
+ require 'secure_trading/objects/payment_card'
27
+ require 'secure_trading/objects/tds_response'
28
+ require 'secure_trading/objects/transaction'
29
+ require 'secure_trading/objects/user_agent'
30
+
31
+ module SecureTrading
32
+ end
@@ -0,0 +1,29 @@
1
+ module SecureTrading
2
+ class Connection
3
+
4
+ class CertificateFileNotFound < RuntimeError; end
5
+
6
+ attr_reader :xpay_client, :site_reference, :certificate_path
7
+
8
+ def initialize(xpay_client, site_reference, certificate_path, options = {})
9
+ @xpay_client = xpay_client
10
+ @site_reference = site_reference
11
+ @certificate_path = certificate_path
12
+ @options = options
13
+ @options[:request_cache_path] ||= File.join('', 'tmp', 'securetrading', 'requests')
14
+ end
15
+
16
+ def certificate
17
+ if File.exist?(self.certificate_path)
18
+ File.read(self.certificate_path)
19
+ else
20
+ raise CertificateFileNotFound, "Certificate file was not located at #{self.certificate_path}"
21
+ end
22
+ end
23
+
24
+ def options
25
+ @options
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ module SecureTrading
2
+ class DataObject
3
+
4
+ def initialize(attributes = {})
5
+ attributes.each do |key,value|
6
+ self.send("#{key}=", value) if self.respond_to?(key)
7
+ end
8
+ end
9
+
10
+ def to_xml(options = {})
11
+ Util.to_xml(self.to_hash, options) if self.respond_to?(:to_hash)
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class Customer < DataObject
4
+
5
+ attr_accessor :title, :first_name, :last_name, :company, :address, :phone_number, :email_address
6
+
7
+ def to_hash
8
+ {
9
+ 'Postal' => {
10
+ 'Name' => { 'NamePrefix' => sanitize(self.title), 'FirstName' => sanitize(self.first_name), 'LastName' => sanitize(self.last_name) },
11
+ 'Company' => sanitize(self.company),
12
+ 'Street' => sanitize(self.address.street),
13
+ 'City' => sanitize(self.address.city),
14
+ 'StateProv' => sanitize(self.address.state),
15
+ 'PostalCode' => sanitize(self.address.postal_code),
16
+ 'CountryCode' => sanitize(self.address.country)
17
+ },
18
+ 'Telecom' => {'Phone' => sanitize(self.phone_number)},
19
+ 'Online' => {'Email' => sanitize(self.email_address)}
20
+ }
21
+ end
22
+
23
+ def sanitize(value)
24
+ Iconv.iconv('ascii//translit//IGNORE', 'utf-8//IGNORE', value).join
25
+ end
26
+
27
+ class Address < DataObject
28
+ attr_accessor :street, :city, :state, :postal_code, :country
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class Order < DataObject
4
+
5
+ attr_accessor :reference, :details
6
+
7
+ def to_hash
8
+ {
9
+ 'OrderReference' => self.reference,
10
+ 'OrderInformation' => self.details
11
+ }
12
+ end
13
+
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class PaymentCard < DataObject
4
+
5
+ attr_accessor :type, :number, :issue, :start_date, :expiry_date, :security_code, :reference
6
+
7
+ def to_hash
8
+ {
9
+ 'Type' => self.type,
10
+ 'Number' => self.number,
11
+ 'Issue' => self.issue,
12
+ 'StartDate' => self.start_date,
13
+ 'ExpiryDate' => self.expiry_date,
14
+ 'SecurityCode' => self.security_code,
15
+ 'ParentTransactionReference' => self.reference
16
+ }
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class TDSResponse < DataObject
4
+
5
+ attr_accessor :md, :pa_res, :enrolled
6
+
7
+ def to_hash
8
+ {
9
+ 'Enrolled' => self.enrolled,
10
+ 'PaRes' => self.pa_res,
11
+ 'MD' => self.md
12
+ }
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class Transaction < DataObject
4
+
5
+ attr_accessor :verifier, :reference
6
+
7
+ def to_hash
8
+ {
9
+ 'TransactionVerifier' => self.verifier,
10
+ 'ParentTransactionReference' => self.reference
11
+ }
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module SecureTrading
2
+ module Objects
3
+ class UserAgent < DataObject
4
+
5
+ attr_accessor :user_agent, :accept_header
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,83 @@
1
+ module SecureTrading
2
+ class Request
3
+
4
+ class ConnectionTimeout < RuntimeError; end
5
+ class ConnectionFailed < RuntimeError; end
6
+ class PrematureXpayQuery < RuntimeError; end
7
+
8
+ attr_accessor :connection
9
+ attr_reader :response
10
+
11
+ def initialize(connection)
12
+ @connection = connection
13
+ end
14
+
15
+ ## Send the XML request, using the provided connection.
16
+ def process
17
+ post_to_xpay!
18
+ self.response.success?
19
+ end
20
+
21
+ ## Returns the error message provided from Xpay
22
+ def error_message
23
+ @error_message
24
+ end
25
+
26
+ ## Return the full XML request for this request. The full output from this method should be sent to the secure trading
27
+ ## Xpay API.
28
+ def xml_request
29
+ attributes = {}
30
+ attributes['RequestBlock'] = {'Version' => SECURETRADING_API_VERSION}
31
+ attributes['Request'] = {'Type' => self.request_type}
32
+ @xml_request ||= Util.to_xml({'Request' => self.to_hash, 'Certificate' => self.connection.certificate}, :root => 'RequestBlock', :attributes => attributes)
33
+ end
34
+
35
+ ## Return a sanitized version of the xml request for storing in a data store. Sensitive card numbers and security codes will be
36
+ ## replaced with asterisks and the certificate will be removed.
37
+ def sanitized_xml_request
38
+ xml = xml_request.dup
39
+ %w{ Number SecurityCode }.each do |field|
40
+ xml.gsub!(/\<#{field}\>(.+)\<\/#{field}\>/) { "<#{field}>" + ('*' * $1.size) + "</#{field}>" }
41
+ end
42
+ xml.gsub!(/\<Certificate\>(.*)\<\/Certificate\>/m, '<Certificate>{FILTERED}</Certificate>')
43
+ xml
44
+ end
45
+
46
+ private
47
+
48
+ ## Post the XML request to the Xpay client and set the full response to @raw_xpay_response. Any connection errors
49
+ ## will raise exceptions. All requests timeout after 1 minute (don't want anything silly going on really)
50
+ def post_to_xpay!
51
+ begin
52
+ Timeout::timeout(60) do
53
+ Net::HTTP.start(self.connection.xpay_client.host, self.connection.xpay_client.port) do |http|
54
+ response = http.post('/', "xml=#{self.xml_request}")
55
+ @response = Response.new(response.body)
56
+ save_xml_request
57
+ end
58
+ end
59
+ rescue Timeout::Error
60
+ raise ConnectionTimeout, "Connection to Xpay at #{self.connection.xpay_client} failed."
61
+ end
62
+ end
63
+
64
+ ## Cache the sanitized XML request in the file system in case secure trading support want help figuring
65
+ ## stuff out for themselves
66
+ def save_xml_request
67
+ ## Make the directory for the requests
68
+ FileUtils.mkdir_p(@connection.options[:request_cache_path])
69
+ ## Where to save the request?
70
+ if @response.transaction.reference
71
+ request_file = File.join(@connection.options[:request_cache_path], @response.transaction.reference.to_s)
72
+ ## Write the XML output...
73
+ output = ["## Logged Output for Transaction #{@response.transaction.reference} on #{Time.now}"]
74
+ output << "## Request sent"
75
+ output << self.sanitized_xml_request
76
+ output << "## Response received"
77
+ output << @response.raw_response
78
+ File.open(request_file, 'w') { |f| f.write(output.join("\n\n"))}
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,22 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class Authorisation < Request
4
+
5
+ attr_accessor :amount, :currency, :settlement_day, :customer, :payment_method, :order
6
+
7
+ def request_type
8
+ 'AUTH'
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {'SiteReference' => self.connection.site_reference, 'Amount' => self.amount, 'Currency' => self.currency || 'GBP', 'SettlementDay' => self.settlement_day || 1 },
14
+ 'CustomerInfo' => customer,
15
+ 'PaymentMethod' => {'CreditCard' => payment_method },
16
+ 'Order' => order
17
+ }
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class AuthorisationReversal < Request
4
+
5
+ attr_accessor :transaction
6
+
7
+ def request_type
8
+ "AUTHREVERSAL"
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {'SiteReference' => self.connection.site_reference},
14
+ 'PaymentMethod' => {'CreditCard' => self.transaction}
15
+ }
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class AuthorisationWithTDS < Authorisation
4
+
5
+ attr_accessor :tds_response
6
+
7
+ def request_type
8
+ 'ST3DAUTH'
9
+ end
10
+
11
+ def to_hash
12
+ hash = super()
13
+ hash['PaymentMethod']['ThreeDSecure'] = self.tds_response
14
+ hash
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class ContinuousAuthorisation < Authorisation
4
+
5
+
6
+ def request_type
7
+ 'CONTINUOUSAUTH'
8
+ end
9
+
10
+ def to_hash
11
+ h = super
12
+ h['Operation'].delete('Amount') if self.amount.nil?
13
+ h['Operation'].delete('Currency') if self.currency.nil?
14
+ h
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class Refund < Request
4
+
5
+ attr_accessor :amount, :transaction
6
+
7
+ def request_type
8
+ 'REFUND'
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {'SiteReference' => self.connection.site_reference, 'Amount' => self.amount},
14
+ 'PaymentMethod' => {'CreditCard' => transaction },
15
+ }
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class Settlement < Request
4
+
5
+ attr_accessor :amount, :transaction_reference, :date, :status
6
+
7
+ def request_type
8
+ 'SETTLEMENT'
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {
14
+ 'SiteReference' => self.connection.site_reference,
15
+ 'TransactionReference' => self.transaction_reference,
16
+ 'SettleDate' => self.date || 'NEXT',
17
+ 'SettleStatus' => self.status.to_i,
18
+ 'SettleAmount' => self.amount
19
+ },
20
+ }
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class TDSQuery < Request
4
+
5
+ attr_accessor :amount, :currency, :user_agent, :payment_method, :order, :merchant_name, :term_url
6
+
7
+ def request_type
8
+ 'ST3DCARDQUERY'
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {
14
+ 'SiteReference' => self.connection.site_reference,
15
+ 'Amount' => self.amount,
16
+ 'Currency' => self.currency || 'GBP',
17
+ 'TermUrl' => self.term_url,
18
+ 'MerchantName' => self.merchant_name
19
+ },
20
+ 'CustomerInfo' => self.user_agent,
21
+ 'PaymentMethod' => {'CreditCard' => self.payment_method },
22
+ 'Order' => self.order
23
+ }
24
+ end
25
+
26
+ def invalid_details?
27
+ ## This can actually mean quite a few other things but for now we're assuming the card details are invalid
28
+ ## as this is the most likely cause for result 0
29
+ self.response.result == 0
30
+ end
31
+
32
+ ## Should we perform a 3d auth for this card?
33
+ def request_3d_auth?
34
+ return false if self.response.result == 2
35
+ self.response.result == 1 && ['Y', 'N', 'U'].include?(self.response.operation_response['Enrolled'])
36
+ end
37
+
38
+ def is_enrolled?
39
+ self.response.operation_response['Enrolled'].upcase == 'Y'
40
+ end
41
+
42
+ def enrollment_status
43
+ self.response.operation_response['Enrolled']
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ module SecureTrading
2
+ module Requests
3
+ class TransactionQuery < Request
4
+
5
+ attr_accessor :reference
6
+
7
+ def request_type
8
+ 'TRANSACTIONQUERY'
9
+ end
10
+
11
+ def to_hash
12
+ {
13
+ 'Operation' => {'SiteReference' => self.connection.site_reference, 'TransactionReference' => self.reference}
14
+ }
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ module SecureTrading
2
+ class Response
3
+
4
+ attr_reader :response
5
+
6
+ def initialize(response)
7
+ @response = response
8
+ end
9
+
10
+ def xml
11
+ @xml ||= XmlSimple.xml_in(@response)
12
+ end
13
+
14
+ def type
15
+ self.xml['Response'].first['Type']
16
+ end
17
+
18
+ def live?
19
+ self.xml["Live"] == "TRUE"
20
+ end
21
+
22
+ def message
23
+ self.operation_response['Message']
24
+ end
25
+
26
+ def operation_response
27
+ Util.stringify_values(self.xml["Response"].first["OperationResponse"].first)
28
+ end
29
+
30
+ def threed3secure_response
31
+ Util.stringify_values(self.xml["Response"].first["ThreeDSecure"].first)
32
+ end
33
+
34
+ def result
35
+ operation_response['Result'].to_i
36
+ end
37
+
38
+ def success?
39
+ result == 1
40
+ end
41
+
42
+ def transaction
43
+ Objects::Transaction.new(:verifier => self.operation_response['TransactionVerifier'], :reference => self.operation_response['TransactionReference'])
44
+ end
45
+
46
+ def raw_response
47
+ @response
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ require 'builder'
2
+ module SecureTrading
3
+ class Util
4
+ class << self
5
+ def stringify_values(hash)
6
+ hash.inject({}) do |options, (key,value)|
7
+ if value.is_a?(Array)
8
+ options[key] = value.first
9
+ else
10
+ options[key] = value
11
+ end
12
+ options
13
+ end
14
+ end
15
+
16
+ def to_xml(hash, options = {})
17
+ options[:indent] ||= 2
18
+ options = {:builder => Builder::XmlMarkup.new(:indent => options[:indent])}.merge(options)
19
+
20
+ attributes = options[:attributes] ||= {}
21
+ root_attributes = attributes[options[:root].to_s] || {}
22
+ options[:builder].__send__(:method_missing, options[:root].to_s, root_attributes) do
23
+ hash.each do |key, value|
24
+ case value
25
+ when ::Hash
26
+ to_xml(value, options.merge({ :root => key, :skip_instruct => true }))
27
+ else
28
+ if value.respond_to?(:to_hash)
29
+ value = value.to_hash
30
+ end
31
+ if value.is_a?(Hash)
32
+ Util.to_xml(value, options.merge({ :root => key, :skip_instruct => true }))
33
+ else
34
+ options[:builder].tag!(key.to_s, value)
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module SecureTrading
2
+ class XpayClient
3
+
4
+ attr_reader :host, :port
5
+
6
+ def initialize(host, port = 5000)
7
+ @host, @port = host, port
8
+ end
9
+
10
+ def to_s
11
+ "<XPayClient::#{host}:#{port}>"
12
+ end
13
+
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secure_trading
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - aTech Media
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-03 00:00:00.000000000Z
13
+ dependencies: []
14
+ description:
15
+ email: adam@atechmedia.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/secure_trading/connection.rb
21
+ - lib/secure_trading/data_object.rb
22
+ - lib/secure_trading/objects/customer.rb
23
+ - lib/secure_trading/objects/order.rb
24
+ - lib/secure_trading/objects/payment_card.rb
25
+ - lib/secure_trading/objects/tds_response.rb
26
+ - lib/secure_trading/objects/transaction.rb
27
+ - lib/secure_trading/objects/user_agent.rb
28
+ - lib/secure_trading/request.rb
29
+ - lib/secure_trading/requests/authorisation.rb
30
+ - lib/secure_trading/requests/authorisation_reversal.rb
31
+ - lib/secure_trading/requests/authorisation_with_tds.rb
32
+ - lib/secure_trading/requests/continuous_authorisation.rb
33
+ - lib/secure_trading/requests/refund.rb
34
+ - lib/secure_trading/requests/settlement.rb
35
+ - lib/secure_trading/requests/tds_query.rb
36
+ - lib/secure_trading/requests/transaction_query.rb
37
+ - lib/secure_trading/response.rb
38
+ - lib/secure_trading/util.rb
39
+ - lib/secure_trading/xpay_client.rb
40
+ - lib/secure_trading.rb
41
+ homepage: http://www.atechmedia.com
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.10
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Secure Trading Ruby Library
65
+ test_files: []