yardi 4.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +29 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/CODEOWNERS +1 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/README.md +212 -0
- data/Rakefile +7 -0
- data/bin/console +15 -0
- data/config/multi_xml.rb +4 -0
- data/docs/contributing.md +24 -0
- data/docs/getting_started.md +14 -0
- data/lib/yardi.rb +54 -0
- data/lib/yardi/document_parser.rb +6 -0
- data/lib/yardi/document_parser/base.rb +85 -0
- data/lib/yardi/document_parser/guest_card_import_response_object.rb +72 -0
- data/lib/yardi/document_parser/prospects.rb +79 -0
- data/lib/yardi/document_parser/residents.rb +59 -0
- data/lib/yardi/error/base.rb +7 -0
- data/lib/yardi/error/connection_error.rb +12 -0
- data/lib/yardi/error/empty_response.rb +11 -0
- data/lib/yardi/error/error_response.rb +11 -0
- data/lib/yardi/error/fault_response.rb +14 -0
- data/lib/yardi/error/guests_not_found.rb +10 -0
- data/lib/yardi/error/invalid_configuration.rb +11 -0
- data/lib/yardi/error/missing_property.rb +10 -0
- data/lib/yardi/error/no_results.rb +10 -0
- data/lib/yardi/error/resource_not_found.rb +9 -0
- data/lib/yardi/error/service_unavailable.rb +11 -0
- data/lib/yardi/error/unparsable_response.rb +11 -0
- data/lib/yardi/model/event.rb +18 -0
- data/lib/yardi/model/guest_card_response.rb +12 -0
- data/lib/yardi/model/prospect.rb +49 -0
- data/lib/yardi/model/resident.rb +36 -0
- data/lib/yardi/parameter/agent.rb +13 -0
- data/lib/yardi/parameter/contact_info.rb +13 -0
- data/lib/yardi/parameter/credential.rb +16 -0
- data/lib/yardi/parameter/property.rb +35 -0
- data/lib/yardi/parameter/prospect.rb +25 -0
- data/lib/yardi/parameter/user.rb +64 -0
- data/lib/yardi/request/base.rb +99 -0
- data/lib/yardi/request/get_residents.rb +39 -0
- data/lib/yardi/request/get_yardi_guest_activity.rb +85 -0
- data/lib/yardi/request/import_yardi_guest.rb +73 -0
- data/lib/yardi/request_section.rb +24 -0
- data/lib/yardi/request_section/authentication.rb +24 -0
- data/lib/yardi/request_section/lead_management.rb +148 -0
- data/lib/yardi/request_section/prospect.rb +27 -0
- data/lib/yardi/request_section/residents.rb +18 -0
- data/lib/yardi/utils.rb +6 -0
- data/lib/yardi/utils/configuration_validator.rb +17 -0
- data/lib/yardi/utils/phone_parser.rb +23 -0
- data/lib/yardi/utils/request_fetcher.rb +47 -0
- data/lib/yardi/utils/request_generator.rb +88 -0
- data/lib/yardi/validator.rb +6 -0
- data/lib/yardi/validator/empty_response.rb +43 -0
- data/lib/yardi/validator/error_response.rb +87 -0
- data/lib/yardi/validator/fault_response.rb +40 -0
- data/lib/yardi/validator/missing_property.rb +60 -0
- data/lib/yardi/version.rb +5 -0
- data/yardi.gemspec +31 -0
- metadata +246 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module DocumentParser
|
5
|
+
# Parse the GetFloorPlanList response
|
6
|
+
class GuestCardImportResponseObject < Base
|
7
|
+
SOAP_ACTION = 'ImportYardiGuest_Login'.freeze
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
attr_reader :body
|
12
|
+
|
13
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
14
|
+
# into a Hash
|
15
|
+
# @return [Yardi::Model::GuestcardResponse]
|
16
|
+
# @raise [Yardi::Error::Base] if the response is invalid
|
17
|
+
def parse_body(body)
|
18
|
+
@body = body
|
19
|
+
Model::GuestCardResponse.new(messages: messages, remote_id: remote_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
# If there is only one message, we get a hash, otherwise we get an array
|
23
|
+
# of messages. Wrap everything to be an Array so we can use the same logic
|
24
|
+
# for finding and parsing out error messages.
|
25
|
+
# Successful agent info responses do not include a messages node, but
|
26
|
+
# successful guestcard responses use the messages section to include their
|
27
|
+
# CustomerID and to tell us that the guestcard was imported.
|
28
|
+
def message_nodes
|
29
|
+
Array(result_node['Messages']['Message'])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convert from this:
|
33
|
+
# [
|
34
|
+
# {
|
35
|
+
# "messageType"=>"FYI",
|
36
|
+
# "__content__"=>"Xml Imported: 6/27/2016 8:12:19 PM"
|
37
|
+
# },
|
38
|
+
# {
|
39
|
+
# "messageType"=>"FYI",
|
40
|
+
# "__content__"=>"Inserted Prospect CustomerID: p0123456789"
|
41
|
+
# }
|
42
|
+
# ]
|
43
|
+
# to this:
|
44
|
+
# [
|
45
|
+
# { 'FYI' => 'Xml Imported: 6/27/2016 8:12:19 PM' },
|
46
|
+
# { 'FYI' => 'Inserted Prospect CustomerID: p0123456789' }
|
47
|
+
# ]
|
48
|
+
def messages
|
49
|
+
message_array = []
|
50
|
+
message_nodes.each do |node|
|
51
|
+
message_array << { node['messageType'] => node['__content__'] }
|
52
|
+
end
|
53
|
+
|
54
|
+
message_array
|
55
|
+
end
|
56
|
+
|
57
|
+
def remote_id
|
58
|
+
id_message = message_nodes.detect do |node|
|
59
|
+
node['messageType'].downcase == 'fyi' &&
|
60
|
+
node['__content__'] =~ /(Inserted|Updated) Prospect CustomerID:/
|
61
|
+
end
|
62
|
+
|
63
|
+
unless id_message.nil?
|
64
|
+
id_regex =
|
65
|
+
/(Inserted|Updated) Prospect CustomerID: (?<remote_id>p\d+)/
|
66
|
+
id_match = id_message['__content__'].match(id_regex)
|
67
|
+
!id_match.nil? ? id_match[:remote_id] : nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require 'yardi/model/prospect'
|
3
|
+
require 'yardi/utils/phone_parser'
|
4
|
+
|
5
|
+
module Yardi
|
6
|
+
module DocumentParser
|
7
|
+
# Build Prospect objects from prospect search response body.
|
8
|
+
class Prospects < Base
|
9
|
+
SOAP_ACTION = 'GetYardiGuestActivity_Search'.freeze
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
attr_reader :body
|
14
|
+
|
15
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
16
|
+
# into a Hash
|
17
|
+
# @return [Array<Yardi::Model::Prospect>]
|
18
|
+
# @raise [Yardi::Error::Base] if the response is invalid
|
19
|
+
def parse_body(body)
|
20
|
+
@body = body
|
21
|
+
prospects
|
22
|
+
end
|
23
|
+
|
24
|
+
def prospects
|
25
|
+
prospects = result_node['LeadManagement']['Prospects']['Prospect']
|
26
|
+
prospects = [prospects] unless prospects.is_a?(Array)
|
27
|
+
|
28
|
+
prospects.map { |prospect| build_prospect(prospect) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_prospect(prospect)
|
32
|
+
customer = [prospect['Customers']['Customer']].flatten.first
|
33
|
+
events = prospect.dig('Events', 'Event') || []
|
34
|
+
|
35
|
+
Model::Prospect.new(
|
36
|
+
first_name: customer['Name']['FirstName'],
|
37
|
+
last_name: customer['Name']['LastName'],
|
38
|
+
email: customer['Email'],
|
39
|
+
phones: Utils::PhoneParser.parse(customer['Phone']),
|
40
|
+
events: build_events(events),
|
41
|
+
prospect_id: remote_id(customer, 'ProspectID'),
|
42
|
+
tenant_id: remote_id(customer, 'TenantID')
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_events(event_nodes)
|
47
|
+
events_array = event_nodes.is_a?(Array) ? event_nodes : [event_nodes]
|
48
|
+
source = transaction_source(events_array)
|
49
|
+
|
50
|
+
events_array.map do |e|
|
51
|
+
Model::Event.new(
|
52
|
+
remote_id: e.fetch('EventID', {})['IDValue'],
|
53
|
+
type: e['EventType'],
|
54
|
+
timestamp: e['EventDate'],
|
55
|
+
first_contact: e['FirstContact'] == 'true',
|
56
|
+
transaction_source: source
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def transaction_source(event_nodes)
|
62
|
+
transaction_node = event_nodes.detect{ |e| e['TransactionSource'].present? }
|
63
|
+
if transaction_node.nil?
|
64
|
+
'None Given From Yardi'
|
65
|
+
else
|
66
|
+
transaction_node['TransactionSource']
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def remote_id(customer, id_type)
|
71
|
+
desired_id_node = customer['Identification'].detect do |id_node|
|
72
|
+
id_node['IDType'] == id_type
|
73
|
+
end
|
74
|
+
|
75
|
+
desired_id_node['IDValue']
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module DocumentParser
|
5
|
+
class Residents < Base
|
6
|
+
SOAP_ACTION = 'GetResidents'.freeze
|
7
|
+
|
8
|
+
def initialize(property_id)
|
9
|
+
@property_id = property_id
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
attr_reader :body, :property_id
|
15
|
+
|
16
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
17
|
+
# into a Hash
|
18
|
+
# @return [Array<Yardi::Model::Resident>]
|
19
|
+
# @raise [Yardi::Error::Base] if the response is invalid
|
20
|
+
def parse_body(body)
|
21
|
+
@body = body
|
22
|
+
residents
|
23
|
+
end
|
24
|
+
|
25
|
+
def residents
|
26
|
+
path = %w[MITS_ResidentData PropertyResidents Residents Resident]
|
27
|
+
results = [result_node.dig(*path)].flatten.compact
|
28
|
+
|
29
|
+
if results.empty?
|
30
|
+
raise Error::NoResults,
|
31
|
+
"Failed to get residents for yardi_property_id: #{property_id}"
|
32
|
+
end
|
33
|
+
|
34
|
+
results.map { |r| create_resident(r) }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Creates a primary Resident after first creating Resident objects
|
40
|
+
# for roommates under `OtherOccupants`.
|
41
|
+
def create_resident(resident)
|
42
|
+
roommate_nodes = resident.dig('OtherOccupants', 'OtherOccupant')
|
43
|
+
roommates = roommate_nodes.nil? ? [] : create_roommates(roommate_nodes)
|
44
|
+
Model::Resident.new(resident, type: 'primary', roommates: roommates)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates roommates given `OtherOccupant` data. Note that this can
|
48
|
+
# either be a Hash or Array, so we cast it to an Array from the start.
|
49
|
+
def create_roommates(roommates)
|
50
|
+
return unless roommates
|
51
|
+
roommates = [roommates].flatten
|
52
|
+
|
53
|
+
roommates.map do |r|
|
54
|
+
Model::Resident.new(r, type: 'roommate')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module Error
|
5
|
+
# Raised when a Yardi response is missing any real data. This can happen
|
6
|
+
# when part of the authentication section of the request is missing or
|
7
|
+
# invalid.
|
8
|
+
class EmptyResponse < Base
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module Error
|
5
|
+
# Raised when a Yardi response contains error message, which can mean that
|
6
|
+
# we are missing a required node in the XML we send them. In this case, the
|
7
|
+
# response contains two message nodes, both of which have the same content.
|
8
|
+
class ErrorResponse < Base
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module Error
|
5
|
+
# Raised when a Yardi response contains a Fault node, which seems to happen
|
6
|
+
# sometimes when we leave out a required node. Sometimes there is an XSD
|
7
|
+
# check on their end, in which case a regular ErrorResponse will be raised,
|
8
|
+
# but other times the whole system seems to fall over and we see a response
|
9
|
+
# that looks like the example in the prospect_search/missing_node_error.xml
|
10
|
+
# fixture.
|
11
|
+
class FaultResponse < Base
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Yardi
|
4
|
+
module Error
|
5
|
+
# Raised when we're missing any of the required configuration information
|
6
|
+
# needed to make a request. @see Yardi::CONFIG_KEYS for the list of required
|
7
|
+
# fields.
|
8
|
+
class InvalidConfiguration < Base
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Yardi
|
2
|
+
module Model
|
3
|
+
class Event
|
4
|
+
attr_reader :remote_id, :type, :timestamp, :first_contact,
|
5
|
+
:transaction_source
|
6
|
+
|
7
|
+
# timestamp is a string that does not include timezone, so we leave it to
|
8
|
+
# the client to parse correctly.
|
9
|
+
def initialize(remote_id:, type:, timestamp:, first_contact:, transaction_source:)
|
10
|
+
@remote_id = remote_id
|
11
|
+
@type = type
|
12
|
+
@timestamp = timestamp
|
13
|
+
@first_contact = first_contact
|
14
|
+
@transaction_source = transaction_source
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Yardi
|
2
|
+
module Model
|
3
|
+
##
|
4
|
+
# Basic data about a Prospect returned from the Yardi API
|
5
|
+
#
|
6
|
+
# Search for Prospect records using Yardi::Request::GetYardiGuestActivity
|
7
|
+
#
|
8
|
+
class Prospect
|
9
|
+
# The Prospect's first name according to Yardi's database
|
10
|
+
attr_reader :first_name
|
11
|
+
|
12
|
+
# The Prospect's last name according to Yardi's database
|
13
|
+
attr_reader :last_name
|
14
|
+
|
15
|
+
# The Prospect's email address according to Yardi's database
|
16
|
+
attr_reader :email
|
17
|
+
|
18
|
+
# An Array of the Prospect's phone numbers, or +nil+ if there are none
|
19
|
+
attr_reader :phones
|
20
|
+
|
21
|
+
# An Array of Yardi::Model::Event objects
|
22
|
+
attr_reader :events
|
23
|
+
|
24
|
+
# The Prospect id from Yardi's database e.g. "p00003693"
|
25
|
+
attr_reader :prospect_id
|
26
|
+
|
27
|
+
# The tenant id from Yardi's database e.g. "t000456"
|
28
|
+
attr_reader :tenant_id
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
first_name:,
|
32
|
+
last_name:,
|
33
|
+
email:,
|
34
|
+
phones:,
|
35
|
+
events:,
|
36
|
+
prospect_id:,
|
37
|
+
tenant_id:
|
38
|
+
)
|
39
|
+
@first_name = first_name
|
40
|
+
@last_name = last_name
|
41
|
+
@email = email
|
42
|
+
@phones = phones
|
43
|
+
@events = events
|
44
|
+
@prospect_id = prospect_id
|
45
|
+
@tenant_id = tenant_id
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yardi/utils/phone_parser'
|
4
|
+
|
5
|
+
module Yardi
|
6
|
+
module Model
|
7
|
+
class Resident
|
8
|
+
attr_reader :status, :lease_id, :lease_lead_id, :first_name, :last_name,
|
9
|
+
:email, :phones, :unit_name, :move_in_date, :lease_from_date,
|
10
|
+
:lease_to_date, :type, :roommates
|
11
|
+
|
12
|
+
def initialize(resident, type:, roommates: nil)
|
13
|
+
@status = resident['Status']
|
14
|
+
@lease_id = resident['tCode']
|
15
|
+
@lease_lead_id = resident['pCode']
|
16
|
+
@first_name = resident['FirstName']
|
17
|
+
@last_name = resident['LastName']
|
18
|
+
@email = resident['Email']
|
19
|
+
@unit_name = resident['UnitCode']
|
20
|
+
@phones = Utils::PhoneParser.parse(resident['Phone'])
|
21
|
+
@move_in_date = parse_date(resident['MoveInDate'])
|
22
|
+
@lease_from_date = parse_date(resident['LeaseFromDate'])
|
23
|
+
@lease_to_date = parse_date(resident['LeaseToDate'])
|
24
|
+
@type = type
|
25
|
+
@roommates = roommates || []
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Residents may not have LeaseFromDate or LeaseToDate.
|
31
|
+
def parse_date(date)
|
32
|
+
date && Date.strptime(date, '%m/%d/%Y')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|