secure_trading 1.0.0

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.
@@ -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: []