exact4r 0.5

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,170 @@
1
+ require 'builder'
2
+
3
+ module EWS # :nodoc:
4
+ module Transaction # :nodoc:
5
+
6
+ # This class handles encoding of transaction requests to the various transport formats,
7
+ # and the decoding of responses to transaction response objects.
8
+ #
9
+ # The supported formats are:
10
+ # * REST/JSON
11
+ # * REST/XML
12
+ # * SOAP/XML
13
+ class Mapping # :nodoc:
14
+
15
+ XML_REQUEST_TAGS_TO_ATTRS = {
16
+ :ExactID => :gateway_id,
17
+ :Password => :password,
18
+ :Transaction_Type => :transaction_type,
19
+ :DollarAmount => :amount,
20
+ :SurchargeAmount => :surcharge_amount,
21
+ :Card_Number => :cc_number,
22
+ :Transaction_Tag => :transaction_tag,
23
+ :Track1 => :track1,
24
+ :Track2 => :track2,
25
+ :PAN => :pan,
26
+ :Authorization_Num => :auth_number,
27
+ :Expiry_Date => :cc_expiry,
28
+ :CardHoldersName => :cardholder_name,
29
+ :VerificationStr1 => :cc_verification_str1,
30
+ :VerificationStr2 => :cc_verification_str2,
31
+ :CVD_Presence_Ind => :cvd_presence_ind,
32
+ :Tax1Amount => :tax1_amount,
33
+ :Tax1Number => :tax1_number,
34
+ :Tax2Amount => :tax2_amount,
35
+ :Tax2Number => :tax2_number,
36
+ :Secure_AuthRequired => :secure_auth_required,
37
+ :Secure_AuthResult => :secure_auth_result,
38
+ :Ecommerce_Flag => :ecommerce_flag,
39
+ :XID => :xid,
40
+ :CAVV => :cavv,
41
+ :CAVV_Algorithm => :cavv_algorithm,
42
+ :Reference_No => :reference_no,
43
+ :Customer_Ref => :customer_ref,
44
+ :Reference_3 => :reference_3,
45
+ :Language => :language,
46
+ :Client_IP => :client_ip,
47
+ :Client_Email => :client_email,
48
+ :User_Name => :user_name
49
+ } unless defined?(XML_REQUEST_TAGS_TO_ATTRS)
50
+
51
+ XML_RESPONSE_TAGS_TO_ATTRS = {
52
+ :LogonMessage => :logon_message,
53
+ :Error_Number => :error_number,
54
+ :Error_Description => :error_description,
55
+ :Transaction_Error => :transaction_error,
56
+ :Transaction_Approved => :transaction_approved,
57
+ :EXact_Resp_Code => :exact_resp_code,
58
+ :EXact_Message => :exact_message,
59
+ :Bank_Resp_Code => :bank_resp_code,
60
+ :Bank_Message => :bank_message,
61
+ :Bank_Resp_Code_2 => :bank_resp_code_2,
62
+ :SequenceNo => :sequence_no,
63
+ :AVS => :avs,
64
+ :CVV2 => :cvv2,
65
+ :Retrieval_Ref_No => :retrieval_ref_no,
66
+ :CAVV_Response => :cavv_response,
67
+ :MerchantName => :merchant_name,
68
+ :MerchantAddress => :merchant_address,
69
+ :MerchantCity => :merchant_city,
70
+ :MerchantProvince => :merchant_province,
71
+ :MerchantCountry => :merchant_country,
72
+ :MerchantPostal => :merchant_postal,
73
+ :MerchantURL => :merchant_url,
74
+ :CTR => :CTR
75
+ }.merge(XML_REQUEST_TAGS_TO_ATTRS) unless defined?(XML_RESPONSE_TAGS_TO_ATTRS)
76
+
77
+ def self.request_to_json(request)
78
+ request.to_json
79
+ end
80
+ def self.request_to_rest(request)
81
+ xml = Builder::XmlMarkup.new
82
+
83
+ xml.instruct!
84
+ xml.tag! 'Transaction' do
85
+ add_request_hash(xml, request)
86
+ end
87
+ xml.target!
88
+ end
89
+ def self.request_to_soap(request)
90
+ xml = Builder::XmlMarkup.new(:indent => 2)
91
+
92
+ xml.instruct!
93
+ xml.tag! 'soap:Envelope', REQUEST_ENVELOPE_NAMESPACES do
94
+ xml.tag! 'soap:Body' do
95
+ xml.tag! 'n1:SendAndCommit', REQUEST_SAC_ATTRIBUTES do
96
+ xml.tag! 'SendAndCommitSource', REQUEST_SAC_SOURCE_ATTRIBUTES do
97
+ add_request_hash(xml, request)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ xml.target!
103
+ end
104
+
105
+ def self.json_to_response(content)
106
+ response = EWS::Transaction::Response.new
107
+ ActiveSupport::JSON.decode(content).each { |k,v| response.send "#{k}=", v if response.respond_to?(k) }
108
+ response
109
+ end
110
+ def self.rest_to_response(content)
111
+ response = EWS::Transaction::Response.new
112
+ response_xml_string_to_hash(response, content)
113
+ response
114
+ end
115
+ def self.soap_to_response(content)
116
+ response = EWS::Transaction::Response.new
117
+ response_xml_string_to_hash(response, content, xpath = "//types:TransactionResult")
118
+ response
119
+ end
120
+
121
+ private
122
+
123
+ # Adds the request's attributes to the XmlMarkup.
124
+ def self.add_request_hash(xml, request)
125
+ XML_REQUEST_TAGS_TO_ATTRS.each do |k, v|
126
+ xml.tag! k.to_s, request.send(v.to_s)
127
+ end
128
+ end
129
+
130
+ # parses xml response string into the response attributes. The XPath identifies the root element
131
+ # of the payload. For REST (default) this is "//TransactionResult", for SOAP this is name space
132
+ # prefixed "//types:TransactionResult".
133
+ def self.response_xml_string_to_hash(response, xml_string, xpath = "//TransactionResult")
134
+ xml = REXML::Document.new(xml_string)
135
+ if root = REXML::XPath.first(xml, xpath)
136
+ root.elements.to_a.each do |node|
137
+ gwlib_prop_name = XML_RESPONSE_TAGS_TO_ATTRS[node.name.to_sym]
138
+ logger.warn("No mapping for the tag #{node.name}.") if gwlib_prop_name.nil?
139
+ value = (ATTR_FORMATS.include?(gwlib_prop_name)) ? node.text.send("to_#{ATTR_FORMATS[gwlib_prop_name]}") : node.text
140
+ response.send "#{gwlib_prop_name}=", value
141
+ end
142
+ end
143
+ end
144
+
145
+ # type-conversion from strings for certain attributes
146
+ ATTR_FORMATS = {
147
+ :transaction_tag => "i",
148
+ :transaction_approved => "i",
149
+ :amount => "f",
150
+ :surcharge_amount => "f",
151
+ :tax1_amount => "f",
152
+ :tax2_amount => "f"
153
+ }
154
+
155
+ REQUEST_ENVELOPE_NAMESPACES = {
156
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
157
+ "xmlns:env" => "http://schemas.xmlsoap.org/soap/envelope/",
158
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
159
+ }
160
+ REQUEST_SAC_ATTRIBUTES = {
161
+ "xmlns:n1" => "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/Request",
162
+ "env:encodingStyle" => "http://schemas.xmlsoap.org/soap/encoding/"
163
+ }
164
+ REQUEST_SAC_SOURCE_ATTRIBUTES = {
165
+ "xmlns:n2" => "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes",
166
+ "xsi:type" => "n2:Transaction"
167
+ }
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,158 @@
1
+ module EWS # :nodoc:
2
+ module Transaction # :nodoc:
3
+
4
+ # The Request class allows you to build transaction requests for
5
+ # submission to the E-xact Web Service.
6
+ #
7
+ # All requests will result in a financial transaction occurring, with the exception
8
+ # of the <tt>:transaction_details</tt> request, which looks up the details of a pre-existing
9
+ # transaction.
10
+ #
11
+ # The following fields are *mandatory* on all requests:
12
+ # :gateway_id the gateway to which this request should be sent
13
+ # :password your password for that gateway
14
+ # :transaction_type the type of transaction you want to perform
15
+ #
16
+ # Different transaction types will have their own additional requirements when it comes to
17
+ # mandatory and optional fields, and it is recommended that the E-xact Web Service Programming
18
+ # Reference Guide, v8.5 be consulted. This document is contained in the Webservice Plugin ZIP file:
19
+ # http://www.e-xact.com/wp-content/uploads/2007/06/E-xact_Payment_Webservice_Plug-In.zip
20
+ #
21
+ # Please note that, if your chosen transaction requires it, credit card expiry dates *must* be entered in YYMM format.
22
+ #
23
+ # =Allowable transaction types
24
+ # :purchase
25
+ # :pre_auth
26
+ # :pre_auth_completion
27
+ # :forced_post
28
+ # :refund
29
+ # :pre_auth_only
30
+ # :purchase_correction
31
+ # :refund_correction
32
+ # :void
33
+ # :tagged_purchase
34
+ # :tagged_pre_auth
35
+ # :tagged_pre_auth_completion
36
+ # :tagged_void
37
+ # :tagged_refund
38
+ # :tagged_online_debit_refund
39
+ # :recurring_seed_pre_auth
40
+ # :recurring_seed_purchase
41
+ # :idebit_purchase
42
+ # :idebit_refund
43
+ # :secure_storage
44
+ # :secure_storage_eft
45
+ # :transaction_details
46
+ class Request
47
+
48
+ attr_accessor :errors
49
+ attr_accessor :gateway_id, :password, :transaction_type, :amount, :surcharge_amount, :cc_number, :transaction_tag, :track1, :track2, :pan, :auth_number, :cc_expiry, :cardholder_name
50
+ attr_accessor :cc_verification_str1, :cc_verification_str2, :cvd_presence_ind, :tax1_amount, :tax1_number, :tax2_amount, :tax2_number, :secure_auth_required, :secure_auth_result
51
+ attr_accessor :ecommerce_flag, :xid, :cavv, :cavv_algorithm, :reference_no, :customer_ref, :reference_3, :language, :client_ip, :client_email, :user_name
52
+
53
+ # Initialize a Request with a hash of values
54
+ def initialize(hash = {})
55
+ hash.each {|k,v| self.send "#{k.to_s}=", v}
56
+ @errors = {}
57
+ end
58
+
59
+ # Set the <tt>transasction_type<tt> using either a symbol or the relevant code, e.g: for a purchase
60
+ # you can use either <tt>:purchase</tt> or <tt>'00'</tt>
61
+ def transaction_type=(type_sym)
62
+ # assume we're given a symbol, so look up the code
63
+ value = @@transaction_codes[type_sym]
64
+ # if nothing found, then maybe we were given an actual code?
65
+ value = type_sym if value.nil? and @@transaction_codes.values.include? type_sym
66
+
67
+ @transaction_type = value
68
+ end
69
+
70
+ # Indicates whether or not this transaction is a <tt>:transaction_details</tt> transaction
71
+ def is_find_transaction?
72
+ self.transaction_type == "CR"
73
+ end
74
+
75
+ def valid?
76
+ errors[:transaction_type] = "transaction_type must be supplied" if self.transaction_type.blank?
77
+ errors[:transaction_type] = "invalid transaction_type supplied" unless @@transaction_codes.values.include? self.transaction_type
78
+
79
+ # need to authenticate
80
+ errors[:gateway_id] = "gateway_id must be supplied" if self.gateway_id.blank?
81
+ errors[:password] = "password must be supplied" if self.password.blank?
82
+
83
+ # ensure we've been given valid amounts
84
+ errors[:amount] = "invalid amount supplied" unless valid_amount?(self.amount)
85
+ errors[:surcharge_amount] = "invalid surcharge_amount supplied" unless valid_amount?(self.surcharge_amount)
86
+ errors[:tax1_amount] = "invalid tax1_amount supplied" unless valid_amount?(self.tax1_amount)
87
+ errors[:tax2_amount] = "invalid tax2_amount supplied" unless valid_amount?(self.tax2_amount)
88
+
89
+ errors[:cc_number] = "invalid cc_number supplied" unless valid_card_number?
90
+ errors[:cc_expiry] = "invalid cc_expiry supplied" unless valid_expiry_date?
91
+
92
+ errors.empty?
93
+ end
94
+
95
+ private
96
+ def valid_amount?(amount)
97
+ return true if amount.blank?
98
+
99
+ (amount.class == Float) or (amount.class == Fixnum) or !amount.match /[^0-9.]/
100
+ end
101
+
102
+ def valid_card_number?
103
+ return true if self.cc_number.blank?
104
+
105
+ # do a mod10 check
106
+ weight = 1
107
+ card_number = self.cc_number.scan(/./).map(&:to_i)
108
+ result = card_number.reverse[1..-1].inject(0) do |sum, num|
109
+ weight = 1 + weight%2
110
+ digit = num * weight
111
+ sum += (digit / 10) + (digit % 10)
112
+ end
113
+ card_number.last == (10 - result % 10 ) % 10
114
+ end
115
+
116
+ def valid_expiry_date?
117
+ return true if self.cc_expiry.blank?
118
+
119
+ # check format
120
+ return false unless self.cc_expiry.match /^\d{4}$/
121
+
122
+ # check date is not in past
123
+ year, month = 2000 + self.cc_expiry[0..1].to_i, self.cc_expiry[2..3].to_i
124
+
125
+ # CC is still considered valid during the month of expiry,
126
+ # so just compare year and month, ignoring the rest.
127
+ now = DateTime.now
128
+ return ((1..12) === month) && DateTime.new(year, month) >= DateTime.new(now.year, now.month)
129
+ end
130
+
131
+ @@transaction_codes = {
132
+ :purchase => '00',
133
+ :pre_auth => '01',
134
+ :pre_auth_completion => '02',
135
+ :forced_post => '03',
136
+ :refund => '04',
137
+ :pre_auth_only => '05',
138
+ :purchase_correction => '11',
139
+ :refund_correction => '12',
140
+ :void => '13',
141
+ :tagged_purchase => '30',
142
+ :tagged_pre_auth => '31',
143
+ :tagged_pre_auth_completion => '32',
144
+ :tagged_void => '33',
145
+ :tagged_refund => '34',
146
+ :tagged_online_debit_refund => '35',
147
+ :recurring_seed_pre_auth => '40',
148
+ :recurring_seed_purchase => '41',
149
+ :idebit_purchase => '50',
150
+ :idebit_refund => '54',
151
+ :secure_storage => '60',
152
+ :secure_storage_eft => '61',
153
+ :transaction_details => 'CR'
154
+ }.freeze unless defined?(@@transaction_codes)
155
+
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,18 @@
1
+ module EWS # :nodoc:
2
+ module Transaction # :nodoc:
3
+
4
+ # This class encapsulates all the data returned from the E-xact Web Service.
5
+ class Response < EWS::Transaction::Request
6
+
7
+ attr_accessor :logon_message, :error_number, :error_description, :transaction_error, :transaction_approved
8
+ attr_accessor :exact_resp_code, :exact_message, :bank_resp_code, :bank_message, :bank_resp_code_2
9
+ attr_accessor :sequence_no, :avs, :cvv2, :retrieval_ref_no, :cavv_response
10
+ attr_accessor :merchant_name, :merchant_address, :merchant_city, :merchant_province, :merchant_country, :merchant_postal, :merchant_url, :CTR
11
+
12
+ # Indicates whether or not the transaction was approved
13
+ def approved?
14
+ self.transaction_approved == 1
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,128 @@
1
+ require 'net/https'
2
+
3
+ module EWS # :nodoc:
4
+ module Transaction # :nodoc:
5
+
6
+ # A Transporter is responsible for communicating with the E-xact Web Service in
7
+ # whichever dialect is chosen by the user. The available options are:
8
+ # :json REST with JSON payload
9
+ # :rest REST with XML payload
10
+ # :soap SOAP
11
+ #
12
+ # The Transporter will connect to the service, using SSL if required, and will
13
+ # encode Reqests to send to the service, and decode Responses received from the
14
+ # service.
15
+ #
16
+ # Once configured to connect to a particular service, it can be used repeatedly
17
+ # to send as many transactions as required.
18
+ class Transporter
19
+
20
+ # Initialize a Transporter.
21
+ #
22
+ # You can specify the URL you would like the Transporter to connect to,
23
+ # although it defaults to https://api.e-xact.com, the location of our web
24
+ # service.
25
+ #
26
+ # You can also specify a hash of options as follows:
27
+ # :server_cert the path to the server's certificate
28
+ # :issuer_cert the path to the certificate of the issuer of the server certificate
29
+ # :transport_type the default transport_type for this transporter
30
+ #
31
+ # The default certificates are those required to connect to https://api.e-xact.com and the
32
+ # default <tt>transport_type</tt> is <tt>:rest</tt>. The default <tt>transport_type</tt> can be overridden on a per-transaction
33
+ # basis, if you choose to do so, by specifying it as a parameter to the <tt>send</tt> method.
34
+ def initialize(url = "https://api.e-xact.com/", options = {})
35
+ @url = URI.parse(url)
36
+ @server_cert = options[:server_cert] || "certs/exact.cer"
37
+ @issuer_cert = options[:issuer_cert] || "certs/equifax_ca.cer"
38
+ @transport_type = options[:issuer_cert] || :rest
39
+ end
40
+
41
+ # Send a transaction request to the server
42
+ #
43
+ # <tt>request</tt>:: the Request object to encode for transmission to the server
44
+ # <tt>transport_type</tt>:: (optional) the transport type to use for this transaction only. If it not specified, the default transport type for this Transporter will be used
45
+ def send(request, transport_type = nil)
46
+ raise "Request not supplied" if request.nil?
47
+ return false unless request.valid?
48
+
49
+ transport_type ||= @transport_type
50
+
51
+ raise "Transport type #{transport_type} is not supported" unless @@transport_types.include? transport_type
52
+
53
+ if !request.is_find_transaction? or transport_type == :soap
54
+ content = post(request, transport_type)
55
+ else
56
+ content = get(request, transport_type)
57
+ end
58
+
59
+ EWS::Transaction::Mapping.send "#{transport_type}_to_response", content
60
+ end
61
+
62
+ private
63
+
64
+ def post(request, transport_type)
65
+ # build the request
66
+ req = Net::HTTP::Post.new(@url.path + "/transaction.#{@@transport_types[transport_type]}")
67
+ req.basic_auth(request.gateway_id, request.password)
68
+ if transport_type == :soap
69
+ # add the SOAPAction header
70
+ soapaction = (request.is_find_transaction?) ? "TransactionInfo" : "SendAndCommit"
71
+ req.add_field "soapaction", "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/#{soapaction}"
72
+ end
73
+ req.content_type = (transport_type == :json) ? "application/json" : "application/xml"
74
+ req.body = EWS::Transaction::Mapping.send "request_to_#{transport_type.to_s}", request
75
+
76
+ response = get_connection.request(req)
77
+
78
+ case response
79
+ when Net::HTTPSuccess then response.body
80
+ else
81
+ response.error!
82
+ end
83
+ end
84
+
85
+ def get(request, transport_type)
86
+ # build the request
87
+ req = Net::HTTP::Get.new(@url.path + "/transaction/#{request.transaction_tag}.#{@@transport_types[transport_type]}")
88
+ req.basic_auth(request.gateway_id, request.password)
89
+ req.content_type = (transport_type == :json) ? "application/json" : "application/xml"
90
+
91
+ response = get_connection.request(req)
92
+
93
+ case response
94
+ when Net::HTTPSuccess then response.body
95
+ else
96
+ response.error!
97
+ end
98
+ end
99
+
100
+ def get_connection
101
+ connection = Net::HTTP.new(@url.host, @url.port)
102
+ if @url.scheme == 'https'
103
+ connection.use_ssl = true
104
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
105
+ connection.ca_file = @issuer_cert
106
+ connection.verify_callback = method(:validate_certificate)
107
+ end
108
+ connection
109
+ end
110
+
111
+ def validate_certificate(is_ok, ctx)
112
+ # Don't check the root CA cert
113
+ unless (ctx.current_cert.subject.to_s == ctx.current_cert.issuer.to_s)
114
+ is_ok &&= File.open(@server_cert).read == ctx.current_cert.to_pem
115
+ end
116
+ is_ok
117
+ end
118
+
119
+ # what transport types we support, and their corresponding suffixes
120
+ @@transport_types = {
121
+ :rest => "xml",
122
+ :json => "json",
123
+ :soap => "xmlsoap"
124
+ }
125
+
126
+ end
127
+ end
128
+ end