yardi 4.0.8
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.
- 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
|