geti 0.1.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,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