kdonovan-trufina 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.
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +116 -0
- data/Rakefile +53 -0
- data/TODO +8 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/config.rb +62 -0
- data/lib/elements.rb +170 -0
- data/lib/exceptions.rb +15 -0
- data/lib/requests.rb +132 -0
- data/lib/responses.rb +90 -0
- data/lib/trufina.rb +143 -0
- data/rails/init.rb +12 -0
- data/tasks/trufina_tasks.rake +4 -0
- data/test/fixtures/requests/access_request.xml +16 -0
- data/test/fixtures/requests/info_request.xml +6 -0
- data/test/fixtures/requests/login_info_request.xml +6 -0
- data/test/fixtures/requests/login_request.xml +32 -0
- data/test/fixtures/requests/login_request_simple.xml +11 -0
- data/test/fixtures/responses/access_notification.xml +5 -0
- data/test/fixtures/responses/access_response.xml +13 -0
- data/test/fixtures/responses/info_response.xml +19 -0
- data/test/fixtures/responses/login_info_response.xml +15 -0
- data/test/fixtures/responses/login_response.xml +5 -0
- data/test/fixtures/schema.xsd +1064 -0
- data/test/test_helper.rb +15 -0
- data/test/trufina_test.rb +8 -0
- data/trufina.yml.template +31 -0
- metadata +92 -0
data/lib/requests.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Contains all Trufina::Requests::* classes.
|
2
|
+
|
3
|
+
class Trufina
|
4
|
+
|
5
|
+
# These classes are used to generate requests to the Trufina API.
|
6
|
+
# There's a class in this module for each possible Trufina API call.
|
7
|
+
module Requests
|
8
|
+
|
9
|
+
API_NAMESPACE_URL = "http://www.trufina.com/truapi/1/0"
|
10
|
+
class BaseRequest
|
11
|
+
include AllowCreationFromHash
|
12
|
+
|
13
|
+
def initialize(hash = {})
|
14
|
+
super # init method in AllowCreationFromHash
|
15
|
+
autofill_from_config
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
validate_contents
|
20
|
+
# validate_against_schema # -- Functioning code doesn't validate, waiting for feedback from Trufina before implementing
|
21
|
+
to_xml
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Automatically assign any required auth or URL information from the global config
|
27
|
+
def autofill_from_config
|
28
|
+
self.pid ||= Config.credentials[:PID] if self.respond_to?(:pid=)
|
29
|
+
self.pak ||= Config.credentials[:PAK] if self.respond_to?(:pak=)
|
30
|
+
|
31
|
+
# Note: URLs are optional fields, but prefilling from config anyway
|
32
|
+
self.cancel_url ||= Config.endpoints[:cancel] if self.respond_to?(:cancel_url=)
|
33
|
+
self.success_url ||= Config.endpoints[:success] if self.respond_to?(:success_url=)
|
34
|
+
self.failure_url ||= Config.endpoints[:failure] if self.respond_to?(:failure_url=)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Ensure all required data is set BEFORE sending the request off to the remote API
|
38
|
+
def validate_contents
|
39
|
+
missing_elements = self.class.elements.map(&:name).select {|e| !self.respond_to?(e) || self.send(e).nil?}
|
40
|
+
raise Exceptions::MissingRequiredElements.new(missing_elements.join(', ')) unless missing_elements.empty?
|
41
|
+
|
42
|
+
missing_attributes = self.class.attributes.map(&:name).select {|a| !self.respond_to?(a) || self.send(a).nil?}
|
43
|
+
raise Exceptions::MissingRequiredAttributes.new(missing_attributes.join(', ')) unless missing_attributes.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
# We have access to Trufina's XML schema, so we might as well validate against it before we hit their servers
|
47
|
+
# http://codeidol.com/other/rubyckbk/XML-and-HTML/Validating-an-XML-Document/
|
48
|
+
def validate_against_schema
|
49
|
+
libxml = XML::Document.string( self.to_xml )
|
50
|
+
libxml.validate(Trufina.schema)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# When we receive a TrufinaAccessNotification from Trufina, we can then use
|
56
|
+
# the included TNID to receive shared user data (note the TNID is valid for
|
57
|
+
# 14 days) with an InfoRequest. We receive this notification when the user
|
58
|
+
# changes their info or share permissions, or after we send a TrufinaAccessRequest.
|
59
|
+
#
|
60
|
+
# Similar to LoginInfoRequest, but no user interaction.
|
61
|
+
class InfoRequest < BaseRequest
|
62
|
+
include HappyMapper
|
63
|
+
tag 'TrufinaInfoRequest'
|
64
|
+
namespace_url API_NAMESPACE_URL
|
65
|
+
|
66
|
+
element :pid, String, :tag => 'PID'
|
67
|
+
element :tnid, String, :tag => 'TNID'
|
68
|
+
element :pak, String, :tag => 'PAK'
|
69
|
+
end
|
70
|
+
|
71
|
+
# We redirect user to Trufina, they complete registration, Trufina redirects
|
72
|
+
# them back to our success url with an attached TLID. We then have 15 minutes
|
73
|
+
# to use this TLID to retreive the shared data with a LoginInfoRequest.
|
74
|
+
#
|
75
|
+
# Similar to InfoRequest, but requires user interaction.
|
76
|
+
class LoginInfoRequest < BaseRequest
|
77
|
+
include HappyMapper
|
78
|
+
tag 'TrufinaLoginInfoRequest'
|
79
|
+
namespace_url API_NAMESPACE_URL
|
80
|
+
|
81
|
+
element :pid, String, :tag => 'PID'
|
82
|
+
element :tlid, String, :tag => 'TLID'
|
83
|
+
element :pak, String, :tag => 'PAK'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Once we've completed the login flow and retreived our information,
|
87
|
+
# if we want additional information later we ask for it with the
|
88
|
+
# AccessRequest.
|
89
|
+
#
|
90
|
+
# The AccessResponse will contain a status of "pending" for the
|
91
|
+
# additional credentials, and Trufina will notify the user via email
|
92
|
+
# that a partner is requesting a new credential. Once the user grants
|
93
|
+
# permission for that credential, the Partner will be notified via a
|
94
|
+
# AccessNotification.
|
95
|
+
class AccessRequest < BaseRequest
|
96
|
+
include HappyMapper
|
97
|
+
tag 'TrufinaAccessRequest'
|
98
|
+
namespace_url API_NAMESPACE_URL
|
99
|
+
|
100
|
+
element :pid, String, :tag => 'PID'
|
101
|
+
element :prt, String, :tag => 'PRT'
|
102
|
+
element :pak, String, :tag => 'PAK'
|
103
|
+
element :pur, String, :tag => 'PUR'
|
104
|
+
|
105
|
+
element :data, Elements::AccessRequest, :single => true
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# When we wan to send a user to Trufina to register and/or provide their
|
110
|
+
# information and allow us access, we send this to Trufina, who sends us
|
111
|
+
# back a PLID we can use to generate the redirect URL to which we should
|
112
|
+
# send the user.
|
113
|
+
class LoginRequest < BaseRequest
|
114
|
+
# TODO -- DOCS UNCLEAR! MAY need a xlmns="" for each of these elements..?
|
115
|
+
include HappyMapper
|
116
|
+
tag 'TrufinaLoginRequest'
|
117
|
+
namespace_url API_NAMESPACE_URL
|
118
|
+
|
119
|
+
element :pid, String, :tag => 'PID'
|
120
|
+
element :prt, String, :tag => 'PRT'
|
121
|
+
element :pak, String, :tag => 'PAK'
|
122
|
+
|
123
|
+
element :cancel_url, String, :tag => 'CancelURL'
|
124
|
+
element :success_url, String, :tag => 'SuccessURL'
|
125
|
+
element :failure_url, String, :tag => 'FailureURL'
|
126
|
+
|
127
|
+
element :data, Elements::AccessRequest, :single => true
|
128
|
+
element :seed, Elements::SeedInfoGroup, :single => true
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/lib/responses.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Contains all Trufina::Responses::* classes.
|
2
|
+
|
3
|
+
class Trufina
|
4
|
+
class Response
|
5
|
+
# Given returned Trufina XML, instantiate the proper HappyMapper wrapper.
|
6
|
+
#
|
7
|
+
# (Note that this does not perform any error checking beyond unknown
|
8
|
+
# root node name -- the higher level error checking is handled in the
|
9
|
+
# Trufina.parseFromTrufina method)
|
10
|
+
def self.parse(raw_xml)
|
11
|
+
noko = Nokogiri::XML(raw_xml)
|
12
|
+
|
13
|
+
if Trufina::Config.debug?
|
14
|
+
puts "Received XML:\n\n"
|
15
|
+
puts noko.to_xml
|
16
|
+
puts "\n\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Try to find an appropriate local happymapper class
|
20
|
+
begin
|
21
|
+
klass = "Trufina::Responses::#{noko.root.name.gsub('Trufina', '')}".constantize
|
22
|
+
return klass.parse(noko.to_xml)
|
23
|
+
rescue
|
24
|
+
raise Exceptions::UnknownResponseType.new("Raw XML: \n\n#{noko}")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# These classes are used to parse responses from the Trufina API.
|
30
|
+
# There's a class in this module for each possible Trufina response
|
31
|
+
# (plus AccessNotification, which is basically an asynchronous response).
|
32
|
+
module Responses
|
33
|
+
|
34
|
+
class RequestFailure
|
35
|
+
include HappyMapper
|
36
|
+
tag 'TrufinaRequestFailure'
|
37
|
+
|
38
|
+
element :error, String, :tag => 'Error', :attributes => {:kind => String}
|
39
|
+
end
|
40
|
+
|
41
|
+
class AccessNotification
|
42
|
+
include HappyMapper
|
43
|
+
tag 'TrufinaAccessNotification'
|
44
|
+
|
45
|
+
element :prt, String, :tag => 'PRT'
|
46
|
+
element :tnid, String, :tag => 'TNID'
|
47
|
+
end
|
48
|
+
|
49
|
+
class AccessResponse
|
50
|
+
include HappyMapper
|
51
|
+
tag 'TrufinaAccessResponse'
|
52
|
+
|
53
|
+
element :prt, String, :tag => 'PRT'
|
54
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
55
|
+
element :error, String, :tag => 'Error'
|
56
|
+
end
|
57
|
+
|
58
|
+
class InfoResponse
|
59
|
+
include HappyMapper
|
60
|
+
tag 'TrufinaInfoResponse'
|
61
|
+
|
62
|
+
element :prt, String, :tag => 'PRT'
|
63
|
+
element :tnid, String, :tag => 'TNID'
|
64
|
+
element :pur, String, :tag => 'PUR'
|
65
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
66
|
+
element :error, String, :tag => 'Error'
|
67
|
+
end
|
68
|
+
|
69
|
+
class LoginInfoResponse
|
70
|
+
include HappyMapper
|
71
|
+
tag 'TrufinaLoginInfoResponse'
|
72
|
+
|
73
|
+
element :tlid, String, :tag => 'TLID'
|
74
|
+
element :prt, String, :tag => 'PRT'
|
75
|
+
element :pur, String, :tag => 'PUR'
|
76
|
+
element :data, Elements::AccessResponseGroup, :single => true
|
77
|
+
element :error, String, :tag => 'Error'
|
78
|
+
end
|
79
|
+
|
80
|
+
class LoginResponse
|
81
|
+
include HappyMapper
|
82
|
+
tag 'TrufinaLoginResponse'
|
83
|
+
|
84
|
+
element :prt, String, :tag => 'PRT'
|
85
|
+
element :plid, String, :tag => 'PLID'
|
86
|
+
element :error, String, :tag => 'Error'
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
data/lib/trufina.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
require 'nokogiri'
|
7
|
+
|
8
|
+
# Provides a DSL to easily interact with the XML API offered by Trufina.com.
|
9
|
+
class Trufina
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Creates and sends a login request for the specified PRT
|
14
|
+
#
|
15
|
+
# Examples:
|
16
|
+
#
|
17
|
+
# Trufina.login_request(Time.now)
|
18
|
+
# Trufina.login_request(Time.now, :requested => [:phone], :seed => {:name => {:first => 'Foo', :surname => 'Bar'}})
|
19
|
+
#
|
20
|
+
# Options:
|
21
|
+
# * requested -- Hash of requested info to be returned once the user is done with Trufina
|
22
|
+
# * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
|
23
|
+
def login_request(prt, opts = {})
|
24
|
+
opts[:requested] ||= {:name => [:first, :surname]}
|
25
|
+
opts[:seed]
|
26
|
+
xml = Requests::LoginRequest.new(:prt => prt, :data => opts[:requested], :seed => opts[:seed]).render
|
27
|
+
sendToTrufina(xml)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Given a PRT, send the login request an return the redirect URL
|
31
|
+
#
|
32
|
+
# Sends login request to get a PLID from Trufina, then uses that to build
|
33
|
+
# a redirect URL specific to this user.
|
34
|
+
#
|
35
|
+
# Once user completes filling out their information and makes it available
|
36
|
+
# to us, Trufina will ping us with an access_notification to let us know
|
37
|
+
# it's there and we should ask for it.
|
38
|
+
#
|
39
|
+
# Options:
|
40
|
+
# * demo -- Boolean value. If true, and Trufina::Config.staging? is true, returns demo URL
|
41
|
+
# * requested -- Hash of requested info to be returned once the user is done with Trufina
|
42
|
+
# * seed -- Hash of seed data used to prefill the user's forms at Trufina's website
|
43
|
+
def login_url(prt, opts = {})
|
44
|
+
plid = login_request(prt, :requested => opts.delete(:requested), :seed => opts.delete(:seed)).plid
|
45
|
+
login_url_from_plid( plid, opts.delete(:demo) )
|
46
|
+
end
|
47
|
+
|
48
|
+
# This should be exposed to the internet to receive Trufina's postback after
|
49
|
+
# a user follows the login_url and completes a profile
|
50
|
+
#
|
51
|
+
# Receives the access notification, and automatically sends a request for
|
52
|
+
# the actual information.
|
53
|
+
def handle_access_notification(raw_xml)
|
54
|
+
info_request( parseFromTrufina(raw_xml).tnid )
|
55
|
+
end
|
56
|
+
|
57
|
+
# Given a TNID, send info_request
|
58
|
+
def info_request(tnid)
|
59
|
+
xml = Requests::InfoRequest.new(:tnid => tnid).render
|
60
|
+
sendToTrufina(xml)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Given a TLID, send login_info_request
|
64
|
+
def login_info_request(tlid)
|
65
|
+
xml = Requests::LoginInfoRequest.new(:tlid => tlid).render
|
66
|
+
sendToTrufina(xml)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Given either an auth hash containing a PUR and a PRT (e.g. from an InfoResponse
|
70
|
+
# or LoginInfoResponse) or a suitable Trufina::*Response object directly (i.e.
|
71
|
+
# we can just pass the results of a Trufina.login_info_request directly for auth),
|
72
|
+
# as well as a data hash containing any data fields we wish to
|
73
|
+
# request about the specified user, sends a request for data off to Trufina.
|
74
|
+
# Trufina will respond immediately with a status of "pending" for the newly
|
75
|
+
# requested information, will notify the user via email that we're requesting
|
76
|
+
# new info, and finally will notify us via an AccessNotification if/when the
|
77
|
+
# user grants us access to the additional data.
|
78
|
+
def access_request(auth = {}, data = {})
|
79
|
+
auth = {:pur => auth.pur, :prt => auth.prt} unless auth.is_a?(Hash)
|
80
|
+
xml = Requests::AccessRequest.new( auth.merge(:data => data) ).render
|
81
|
+
sendToTrufina(xml)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def domain # :nodoc:
|
88
|
+
Config.staging? ? 'staging.trufina.com' : 'www.trufina.com'
|
89
|
+
end
|
90
|
+
|
91
|
+
def endpoint # :nodoc:
|
92
|
+
'/WebServices/API/'
|
93
|
+
end
|
94
|
+
|
95
|
+
def schema
|
96
|
+
@@schema ||= XML::Schema.from_string(open("http://www.trufina.com/api/truapi.xsd").read)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Send the specified XML to Trufina's servers
|
100
|
+
def sendToTrufina(xml)
|
101
|
+
puts "Sending XML to #{domain}#{endpoint}:\n\n#{xml}\n\n" if Trufina::Config.debug?
|
102
|
+
|
103
|
+
# Connection Info
|
104
|
+
api = Net::HTTP.new( domain, 443 )
|
105
|
+
api.use_ssl = true
|
106
|
+
api.verify_mode = OpenSSL::SSL::VERIFY_NONE # Prevent annoying warnings
|
107
|
+
|
108
|
+
# Request info
|
109
|
+
method_call = Net::HTTP::Post.new( endpoint, {'Content-Type' => 'text/xml'} )
|
110
|
+
method_call.body = xml
|
111
|
+
|
112
|
+
if Config.staging?
|
113
|
+
method_call.basic_auth(Config.staging_access[:username], Config.staging_access[:password])
|
114
|
+
end
|
115
|
+
|
116
|
+
# OK, execute the actual call
|
117
|
+
response = api.request(method_call)
|
118
|
+
raise Exceptions::NetworkError.new(response.msg) unless response.is_a?(Net::HTTPSuccess)
|
119
|
+
parseFromTrufina(response.body)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Try to make something useful from Trufina's XML responses
|
123
|
+
def parseFromTrufina(raw_xml)
|
124
|
+
response = Trufina::Response.parse(raw_xml)
|
125
|
+
|
126
|
+
# Raise exception if we've received an error
|
127
|
+
if response.is_a?(Trufina::Responses::RequestFailure) # Big error -- the entire returned XML is to tell us
|
128
|
+
raise Exceptions::TrufinaResponseException.new("#{response.error.kind}: #{response.error}")
|
129
|
+
elsif response.respond_to?(:error) && response.error # Smaller error, noted inline
|
130
|
+
raise Exceptions::TrufinaResponseException.new("Error in #{response.class.name}: #{response.error}")
|
131
|
+
end
|
132
|
+
|
133
|
+
return response
|
134
|
+
end
|
135
|
+
|
136
|
+
# Given a PLID (from a login_request), return a url to send the user to
|
137
|
+
def login_url_from_plid(plid, is_demo = nil)
|
138
|
+
path = (Config.staging? && is_demo) ? "/DemoPartnerLogin/DemoLogin/#{plid}" : "/PartnerLogin/Login/#{plid}"
|
139
|
+
"http://#{domain}#{path}"
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
base = File.join(File.dirname(__FILE__), '..')
|
2
|
+
|
3
|
+
# Require jimmyz's happymapper to enable to_xml for happymap classes
|
4
|
+
require 'happymapper'
|
5
|
+
|
6
|
+
# Require the rest of the plugin files
|
7
|
+
require File.join(base, 'lib', 'exceptions.rb')
|
8
|
+
require File.join(base, 'lib', 'config.rb')
|
9
|
+
require File.join(base, 'lib', 'elements.rb')
|
10
|
+
require File.join(base, 'lib', 'requests.rb')
|
11
|
+
require File.join(base, 'lib', 'responses.rb')
|
12
|
+
require File.join(base, 'lib', 'trufina.rb')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<TrufinaAccessRequest xmlns="http://www.trufina.com/truapi/1/0">
|
3
|
+
<PID><%= Trufina::Config.credentials[:PID] %></PID>
|
4
|
+
<PRT><%= @prt %></PRT>
|
5
|
+
<PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
|
6
|
+
<PUR><%= @pur %></PUR>
|
7
|
+
<AccessRequest>
|
8
|
+
<Name>
|
9
|
+
<First />
|
10
|
+
</Name>
|
11
|
+
<Age />
|
12
|
+
<ResidenceAddress>
|
13
|
+
<State />
|
14
|
+
</ResidenceAddress>
|
15
|
+
</AccessRequest>
|
16
|
+
</TrufinaAccessRequest>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<TrufinaLoginInfoRequest xmlns="http://www.trufina.com/truapi/1/0">
|
3
|
+
<PID><%= Trufina::Config.credentials[:PID] %></PID>
|
4
|
+
<TLID><%= @tlid %></TLID>
|
5
|
+
<PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
|
6
|
+
</TrufinaLoginInfoRequest>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<TrufinaLoginRequest xmlns="http://www.trufina.com/truapi/1/0">
|
3
|
+
<PID><%= Trufina::Config.credentials[:PID] %></PID>
|
4
|
+
<PRT><%= @prt %></PRT>
|
5
|
+
<PAK><%= Trufina::Config.credentials[:PAK] %></PAK>
|
6
|
+
<CancelURL><%= Trufina::Config.endpoints[:cancel] %></CancelURL>
|
7
|
+
<SuccessURL><%= Trufina::Config.endpoints[:success] %></SuccessURL>
|
8
|
+
<FailureURL><%= Trufina::Config.endpoints[:failure] %></FailureURL>
|
9
|
+
<AccessRequest>
|
10
|
+
<Name>
|
11
|
+
<First />
|
12
|
+
</Name>
|
13
|
+
<Age/>
|
14
|
+
<ResidenceAddress>
|
15
|
+
<State/>
|
16
|
+
</ResidenceAddress>
|
17
|
+
</AccessRequest>
|
18
|
+
<SeedInfo>
|
19
|
+
<Name>
|
20
|
+
<First>John</First>
|
21
|
+
<Surname>Doe</Surname>
|
22
|
+
</Name>
|
23
|
+
<DateOfBirth>1964-09-01</DateOfBirth>
|
24
|
+
<ResidenceAddress>
|
25
|
+
<StreetAddress>1 Main Street</StreetAddress>
|
26
|
+
<StreetAddress>Suite E</StreetAddress>
|
27
|
+
<City>Burlingame</City>
|
28
|
+
<State>CA</State>
|
29
|
+
<PostalCode>94010</PostalCode>
|
30
|
+
</ResidenceAddress>
|
31
|
+
</SeedInfo>
|
32
|
+
</TrufinaLoginRequest>
|