geti 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,133 @@
1
+ class Geti::AuthClient < Geti::Client
2
+ def initialize(auth, terminal_opts, env='test')
3
+ super
4
+
5
+ @sec_code = terminal_opts[:sec_code]
6
+ @verify_check = terminal_opts[:verify].include? :check
7
+ @verify_id = terminal_opts[:verify].include? :identity
8
+ @dl_required = terminal_opts[:verify].include? :dl
9
+ end
10
+
11
+ # Loads terminal settings for the configured terminal. Returns a
12
+ # TerminalSettings object that can be used to confirm requested
13
+ # verification features, terminal ID, and XSD/XML templates.
14
+ def get_terminal_settings
15
+ Geti::TerminalSettings.new(soap_request("GetCertificationTerminalSettings"))
16
+ end
17
+
18
+ # Used to verify that an XML request is valid for use on the
19
+ # terminal. The returned Result will have a validation response
20
+ # but no authorization.
21
+ # NOTE: CERTIFICATION SERVER ONLY
22
+ def validate(opts)
23
+ response = soap_request("AuthGatewayCertification") do |xml|
24
+ data_packet(xml, opts)
25
+ end
26
+ Geti::Response.new(response)
27
+ end
28
+
29
+ # Creates an authorization for funds transfer. Returns a Result
30
+ # with both validation and (if valid) authorization responses.
31
+ def process(opts)
32
+ response = soap_request("ProcessSingleCertificationCheck") do |xml|
33
+ data_packet(xml, opts)
34
+ end
35
+ Geti::Response.new(response)
36
+ end
37
+
38
+
39
+ def data_packet(xml, opts)
40
+ xml.AUTH_GATEWAY do # has an optional REQUEST_ID attribute for later lookups
41
+ xml.TRANSACTION do
42
+ xml.TRANSACTION_ID
43
+ xml.MERCHANT do
44
+ xml.TERMINAL_ID terminal_id
45
+ end
46
+ xml.PACKET do
47
+ xml.IDENTIFIER identifier(opts[:type])
48
+ xml.ACCOUNT do
49
+ xml.ROUTING_NUMBER opts[:routing_number]
50
+ xml.ACCOUNT_NUMBER opts[:account_number]
51
+ xml.ACCOUNT_TYPE opts[:account_type]
52
+ end
53
+ xml.CONSUMER do
54
+ xml.FIRST_NAME opts[:first_name]
55
+ xml.LAST_NAME opts[:last_name]
56
+ xml.ADDRESS1
57
+ xml.ADDRESS2
58
+ xml.CITY
59
+ xml.STATE
60
+ xml.ZIP
61
+ xml.PHONE_NUMBER
62
+ xml.DL_STATE
63
+ xml.DL_NUMBER
64
+ xml.COURTESY_CARD_ID
65
+ if @verify_id
66
+ xml.IDENTITY do
67
+ xml.SSN4
68
+ xml.DOB_YEAR
69
+ end
70
+ end
71
+ end
72
+ xml.CHECK do
73
+ xml.CHECK_AMOUNT("%.2d" % ((opts[:amount]||0)/100.0))
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def identifier(name)
81
+ { :authorize => 'A',
82
+ :void => 'V',
83
+ :override => 'O',
84
+ :payroll => 'P',
85
+ :recurring => 'R'
86
+ }[name]
87
+ end
88
+
89
+ def service_address
90
+ "https://demo.eftchecks.com/webservices/AuthGateway.asmx?WSDL"
91
+ end
92
+
93
+ def soap_header
94
+ { "AuthGatewayHeader" => {
95
+ "UserName" => @user,
96
+ "Password" => @pass,
97
+ "TerminalID" => terminal_id.to_s
98
+ },
99
+ :attributes! => { 'AuthGatewayHeader' => {'xmlns'=>"http://tempuri.org/GETI.eMagnus.WebServices/AuthGateway"}}
100
+ }
101
+ end
102
+
103
+ def terminal_id
104
+ base = {
105
+ # Guaranteed
106
+ 'PPD' => 1010, # Debit-only
107
+ 'POP' => 1110,
108
+ 'TEL' => 1210,
109
+ 'C21' => 1610,
110
+ 'CCD' => 1710, # Debit only
111
+ 'PPD_cr' => 1810, # Debit and Credit
112
+ 'CCD_cr' => 1910, # Debit and Credit
113
+ # Non-Guaranteed
114
+ 'PPD_ng' => 2010,
115
+ 'TEL_ng' => 2210,
116
+ 'WEB' => 2310, # Web is always non-guaranteed
117
+ 'CCD_ng' => 2710,
118
+ 'PPD_ng_cr' => 2810,
119
+ 'CCD_ng_cr' => 2910
120
+ }[@sec_code]
121
+ offset = {
122
+ [false, false, false] => 0,
123
+ [true, false, false] => 1,
124
+ [false, true, true ] => 2,
125
+ [true, true, true ] => 3,
126
+ [false, true, false] => 4,
127
+ [true, true, false] => 5,
128
+ [false, false, true ] => 6,
129
+ [true, false, true ] => 7
130
+ }[[@dl_required, @verify_check, @verify_id]]
131
+ base + offset
132
+ end
133
+ end
@@ -0,0 +1,42 @@
1
+ require 'savon'
2
+ require 'httpi'
3
+
4
+ class Geti::Client
5
+ def initialize(auth, terminal_opts, env='test')
6
+ @user = auth[:user]
7
+ @pass = auth[:pass]
8
+ @env = env
9
+ end
10
+
11
+ def soap_client
12
+ @soap_client ||= Savon.client(service_address)
13
+ end
14
+
15
+ def soap_request(operation, op_key=nil)
16
+ operation.sub!('Certification','') unless certification?
17
+ response = soap_client.request operation do
18
+ http.headers.delete('SOAPAction')
19
+ config.soap_header = soap_header
20
+ xml = Builder::XmlMarkup.new
21
+ xml.instruct!
22
+ yield xml if block_given?
23
+ content = xml.target!
24
+ soap.body = {"DataPacket" => content}
25
+ end
26
+
27
+ op_key ||= operation.gsub(/(.)([A-Z])/, '\1_\2').downcase
28
+ operation.sub!('_certification','') unless certification?
29
+ response_key = (op_key+'_response').to_sym
30
+ result_key = (op_key+'_result').to_sym
31
+
32
+ xml_parser.parse(response.body[response_key][result_key])
33
+ end
34
+
35
+ def certification?
36
+ @env != 'production'
37
+ end
38
+
39
+ def xml_parser
40
+ @xml_parser or Nori
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ require 'ostruct'
2
+
3
+ class Geti::Response
4
+ attr_reader :validation, :authorization, :exception
5
+
6
+ def initialize(response)
7
+ @validation = OpenStruct.new(response[:response][:validation_message])
8
+ @authorization = OpenStruct.new(response[:response][:authorization_message])
9
+ @exception = OpenStruct.new(response[:response][:exception])
10
+ end
11
+
12
+ def errors
13
+ err = []
14
+ Array(@validation.validation_error).each do |e|
15
+ err << e[:message]
16
+ end
17
+ err << @exception.message
18
+ err.compact
19
+ end
20
+
21
+ def success?
22
+ validation.result == "Passed"
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ require 'ostruct'
2
+
3
+ class Geti::TerminalSettings < OpenStruct
4
+ def initialize(attributes)
5
+ super(attributes[:terminal_settings])
6
+ end
7
+ end
@@ -0,0 +1,166 @@
1
+ require 'helper'
2
+
3
+ describe Geti::AppClient do
4
+ def mock_soap!(client, parsed_response, operation, op_key=nil)
5
+ op_key ||= operation.gsub(/(.)([A-Z])/, '\1_\2').downcase
6
+ response_key = (op_key+'_response').to_sym
7
+ result_key = (op_key+'_result').to_sym
8
+
9
+ data = OpenStruct.new(:body => {response_key => {result_key => :encoded_xml}})
10
+
11
+ client.soap_client.should_receive(:request).with(operation).and_return(data)
12
+ client.xml_parser.should_receive(:parse).with(:encoded_xml).and_return(parsed_response)
13
+ end
14
+
15
+ def request_payload
16
+ {
17
+ :id => 123456,
18
+ :name => "Cogsley's Cogs",
19
+ :industry => "Metal_Fabricators",
20
+ :address => "123 Main St",
21
+ :city => "Vancouver",
22
+ :state => "WA",
23
+ :zip => "10120",
24
+ :phone => "5555551234",
25
+ :business_type => "Corporation",
26
+ :days_in_business => 404,
27
+
28
+ :contact_name => 'George Jetson',
29
+ :physical_address => "123 Main St",
30
+ :physical_city => "Vancouver",
31
+ :physical_state => "WA",
32
+ :physical_zip => "10120",
33
+ :physical_phone => "5555551234",
34
+
35
+ :principal_first_name => "Carl",
36
+ :principal_last_name => "Cogsley",
37
+ :principal_title => 'President',
38
+ :principal_address => "123 Main St",
39
+ :principal_city => "Vancouver",
40
+ :principal_state => "WA",
41
+ :principal_zip => "10120",
42
+ :principal_dob => "1965-04-28",
43
+ :principal_ssn => '111222123',
44
+
45
+ :average_amount => "4000",
46
+ :max_amount => "7600",
47
+
48
+ :taxpayer_name => "Carl Cogsley",
49
+ :taxpayer_id => "123456789",
50
+
51
+ :routing_number => "490000018",
52
+ :account_number => "123456789",
53
+ }
54
+ end
55
+
56
+ def success_response
57
+ {:response=>{
58
+ :status=>"Approved",
59
+ :message=>"1 merchant(s) created.\n0 merchant(s) not created due to errors.\n\n-----------------------\nMerchants Created:\nCogsley's Cogs (ISO ID: 9999, CrossRef: 123456, Status: AppApprovedandActivated)\n\n",
60
+ :app_data=>{
61
+ :merchant=>{
62
+ :@name=>"Cogsley's Cogs",
63
+ :@active=>"1",
64
+ :@type=>"Merchant",
65
+ :@cross_ref_id=>"123456",
66
+ :@id=>"20",
67
+ :location=>{
68
+ :terminal=>{
69
+ :@manual_entry=>"N",
70
+ :@name=>"Lipman Nurit 3000-01 (111163) ",
71
+ :@active=>"1",
72
+ :@type=>"Terminal",
73
+ :@cross_ref_id=>"41680",
74
+ :@id=>"111163",
75
+ :@mid=>"101-111163-606"},
76
+ :@name=>"Cogsley's Cogs ",
77
+ :@active=>"1",
78
+ :@ach_name=>"COGSLEYSCOGS",
79
+ :@type=>"Location",
80
+ :@cross_ref_id=>"123456",
81
+ :@id=>"31"},
82
+ :poc1=>{
83
+ :@password=>"UGPRDGIX",
84
+ :@user_name=>"CCogsley",
85
+ :@last_name=>"Cogsley",
86
+ :@first_name=>"Carl"}}},
87
+ :"@xmlns:xsd"=>"http://www.w3.org/2001/XMLSchema",
88
+ :"@xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
89
+ :validation_message=>{:result=>"Passed", :schema_file_path=>nil}}}
90
+ end
91
+
92
+ def error_response
93
+ {:response=>{
94
+ :"@xmlns:xsd"=>"http://www.w3.org/2001/XMLSchema",
95
+ :"@xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
96
+ :validation_message=>
97
+ {:result=>"Failed",
98
+ :schema_file_path=>
99
+ "http://demo.eftchecks.com/WebServices/schemas/app/NewMerchApp_ACH.xsd",
100
+ :validation_error=>
101
+ [{:@line_number=>"1",
102
+ :severity=>"Error",
103
+ :message=>"The 'pocAddress1' attribute is not declared.",
104
+ :@line_position=>"1193"},
105
+ {:@line_number=>"1",
106
+ :severity=>"Error",
107
+ :message=>"The required attribute 'pocAddress' is missing.",
108
+ :@line_position=>"1020"}]}}}
109
+ end
110
+
111
+ def repeat_response
112
+ {:response=>{
113
+ :status=>"Pending",
114
+ :message=>
115
+ "1 merchant(s) created.\n0 merchant(s) not created due to errors.\n\n-----------------------\nMerchants Created:\nCogsley's Cogs (ISO ID: 9999, CrossRef: 123456, Status: PendingInput)\n\n",
116
+ :validation_message=>{:result=>"Passed", :schema_file_path=>nil},
117
+ :"@xmlns:xsd"=>"http://www.w3.org/2001/XMLSchema",
118
+ :"@xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
119
+ :app_data=>{:merchant=>{:@id=>"21"}}}}
120
+ end
121
+
122
+ describe '#board_merchant_ach' do
123
+ it 'calls BoardCertificationMerchant_ACH' do
124
+ client = Geti::AppClient.new(test_credentials, {})
125
+ mock_soap!(client, success_response, "BoardCertificationMerchant_ACH", "board_certification_merchant_ach")
126
+ client.board_merchant_ach(request_payload)
127
+ end
128
+
129
+ it 'calls BoardMerchant_ACH in production' do
130
+ client = Geti::AppClient.new(test_credentials, {}, 'production')
131
+ mock_soap!(client, success_response, "BoardMerchant_ACH", "board_certification_merchant_ach")
132
+ client.board_merchant_ach(request_payload)
133
+ end
134
+
135
+ describe 'response on success' do
136
+ let(:client) {
137
+ Geti::AppClient.new(test_credentials, {}).tap{|c|
138
+ mock_soap!(c, response, "BoardCertificationMerchant_ACH", "board_certification_merchant_ach")
139
+ }
140
+ }
141
+ subject { client.board_merchant_ach(request_payload) }
142
+
143
+ describe 'on success' do
144
+ let(:response) { success_response }
145
+ its([:success]) { should be_true }
146
+ its([:status]) { should eq("Approved") }
147
+
148
+ it 'normalizes (nested) keys' do
149
+ subject[:app_data][:merchant][:id].should eq("20")
150
+ end
151
+ end
152
+
153
+ describe 'on repeat' do
154
+ let(:response) { repeat_response }
155
+ its([:success]) { should be_true }
156
+ its([:status]) { should eq("Pending") }
157
+ end
158
+
159
+ describe 'on error' do
160
+ let(:response) { error_response }
161
+ its([:success]) { should be_false }
162
+ its([:status]) { should be_nil }
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,28 @@
1
+ require 'helper'
2
+
3
+ describe Geti::AuthClient do
4
+ def mock_soap!(client, parsed_response, operation, op_key=nil)
5
+ op_key ||= operation.gsub(/(.)([A-Z])/, '\1_\2').downcase
6
+ response_key = (op_key+'_response').to_sym
7
+ result_key = (op_key+'_result').to_sym
8
+
9
+ data = OpenStruct.new(:body => {response_key => {result_key => :encoded_xml}})
10
+
11
+ client.soap_client.should_receive(:request).with(operation).and_return(data)
12
+ client.xml_parser.should_receive(:parse).with(:encoded_xml).and_return(parsed_response)
13
+ end
14
+
15
+ describe '#get_terminal_settings' do
16
+ it 'calls GetCertificationTerminalSettings' do
17
+ client = Geti::AuthClient.new(test_credentials, {:sec_code => 'WEB', :verify => []})
18
+ mock_soap!(client, {}, "GetCertificationTerminalSettings")
19
+ client.get_terminal_settings
20
+ end
21
+
22
+ it 'calls GetTerminalSettings in production' do
23
+ client = Geti::AuthClient.new(test_credentials, {:sec_code => 'WEB', :verify => []}, 'production')
24
+ mock_soap!(client, {}, "GetTerminalSettings")
25
+ client.get_terminal_settings
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development, :test)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+
11
+ require 'rspec'
12
+ require 'rspec/mocks'
13
+ require 'ostruct'
14
+ class OpenStruct
15
+ def inspect
16
+ "<OpenStruct \"#{@table.inspect}\">"
17
+ end
18
+ end
19
+
20
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
21
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
22
+
23
+ require 'pp'
24
+ require 'geti'
25
+
26
+
27
+ Savon.configure do |config|
28
+ config.log = HTTPI.log = false unless ENV['SOAP_DEBUG']
29
+ end
30
+
31
+ def test_credentials
32
+ YAML.load(File.read('config/test_credentials.yml'))
33
+ end
34
+
35
+ def xit(*args, &block)
36
+ # noop, disabled spec.
37
+ end
@@ -0,0 +1,50 @@
1
+ require 'helper'
2
+
3
+ describe Geti::AppClient do
4
+ describe '#board_merchant_ach' do
5
+ it 'has a successful response' do
6
+ t = Time.now.to_i
7
+ client = Geti::AppClient.new(test_credentials, {})
8
+ response = client.board_merchant_ach({
9
+ :id => t,
10
+ :name => "Cogsley's Cogs %d" % t,
11
+ :industry => "Metal_Fabricators",
12
+ :address => "123 Main St",
13
+ :city => "Vancouver",
14
+ :state => "WA",
15
+ :zip => "10120",
16
+ :phone => "5555551234",
17
+ :business_type => "Corporation",
18
+ :days_in_business => 404,
19
+
20
+ :contact_name => 'George Jetson',
21
+ :physical_address => "123 Main St",
22
+ :physical_city => "Vancouver",
23
+ :physical_state => "WA",
24
+ :physical_zip => "10120",
25
+ :physical_phone => "5555551234",
26
+
27
+ :principal_first_name => "Carl",
28
+ :principal_last_name => "Cogsley",
29
+ :principal_title => 'President',
30
+ :principal_address => "123 Main St",
31
+ :principal_city => "Vancouver",
32
+ :principal_state => "WA",
33
+ :principal_zip => "10120",
34
+ :principal_dob => "1965-04-28",
35
+ :principal_ssn => '111222123',
36
+
37
+ :average_amount => "4000",
38
+ :max_amount => "7600",
39
+
40
+ :taxpayer_name => "Carl Cogsley",
41
+ :taxpayer_id => "123456789",
42
+
43
+ :routing_number => "490000018",
44
+ :account_number => "123456789",
45
+ })
46
+ expect(response[:status]).to eq("Approved")
47
+ expect(response[:message]).to match(/CrossRef: #{t}/)
48
+ end
49
+ end
50
+ end