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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +29 -0
  3. data/.gitignore +5 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +20 -0
  6. data/CODEOWNERS +1 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +4 -0
  9. data/README.md +212 -0
  10. data/Rakefile +7 -0
  11. data/bin/console +15 -0
  12. data/config/multi_xml.rb +4 -0
  13. data/docs/contributing.md +24 -0
  14. data/docs/getting_started.md +14 -0
  15. data/lib/yardi.rb +54 -0
  16. data/lib/yardi/document_parser.rb +6 -0
  17. data/lib/yardi/document_parser/base.rb +85 -0
  18. data/lib/yardi/document_parser/guest_card_import_response_object.rb +72 -0
  19. data/lib/yardi/document_parser/prospects.rb +79 -0
  20. data/lib/yardi/document_parser/residents.rb +59 -0
  21. data/lib/yardi/error/base.rb +7 -0
  22. data/lib/yardi/error/connection_error.rb +12 -0
  23. data/lib/yardi/error/empty_response.rb +11 -0
  24. data/lib/yardi/error/error_response.rb +11 -0
  25. data/lib/yardi/error/fault_response.rb +14 -0
  26. data/lib/yardi/error/guests_not_found.rb +10 -0
  27. data/lib/yardi/error/invalid_configuration.rb +11 -0
  28. data/lib/yardi/error/missing_property.rb +10 -0
  29. data/lib/yardi/error/no_results.rb +10 -0
  30. data/lib/yardi/error/resource_not_found.rb +9 -0
  31. data/lib/yardi/error/service_unavailable.rb +11 -0
  32. data/lib/yardi/error/unparsable_response.rb +11 -0
  33. data/lib/yardi/model/event.rb +18 -0
  34. data/lib/yardi/model/guest_card_response.rb +12 -0
  35. data/lib/yardi/model/prospect.rb +49 -0
  36. data/lib/yardi/model/resident.rb +36 -0
  37. data/lib/yardi/parameter/agent.rb +13 -0
  38. data/lib/yardi/parameter/contact_info.rb +13 -0
  39. data/lib/yardi/parameter/credential.rb +16 -0
  40. data/lib/yardi/parameter/property.rb +35 -0
  41. data/lib/yardi/parameter/prospect.rb +25 -0
  42. data/lib/yardi/parameter/user.rb +64 -0
  43. data/lib/yardi/request/base.rb +99 -0
  44. data/lib/yardi/request/get_residents.rb +39 -0
  45. data/lib/yardi/request/get_yardi_guest_activity.rb +85 -0
  46. data/lib/yardi/request/import_yardi_guest.rb +73 -0
  47. data/lib/yardi/request_section.rb +24 -0
  48. data/lib/yardi/request_section/authentication.rb +24 -0
  49. data/lib/yardi/request_section/lead_management.rb +148 -0
  50. data/lib/yardi/request_section/prospect.rb +27 -0
  51. data/lib/yardi/request_section/residents.rb +18 -0
  52. data/lib/yardi/utils.rb +6 -0
  53. data/lib/yardi/utils/configuration_validator.rb +17 -0
  54. data/lib/yardi/utils/phone_parser.rb +23 -0
  55. data/lib/yardi/utils/request_fetcher.rb +47 -0
  56. data/lib/yardi/utils/request_generator.rb +88 -0
  57. data/lib/yardi/validator.rb +6 -0
  58. data/lib/yardi/validator/empty_response.rb +43 -0
  59. data/lib/yardi/validator/error_response.rb +87 -0
  60. data/lib/yardi/validator/fault_response.rb +40 -0
  61. data/lib/yardi/validator/missing_property.rb +60 -0
  62. data/lib/yardi/version.rb +5 -0
  63. data/yardi.gemspec +31 -0
  64. 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,7 @@
1
+ module Yardi
2
+ module Error
3
+ # A base class for all errors that originate from this gem
4
+ class Base < StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'base'
2
+
3
+ module Yardi
4
+ module Error
5
+ ##
6
+ # An Error that is raised when we have trouble connecting to the API
7
+ # endpoint.
8
+ #
9
+ class ConnectionError < Base
10
+ end
11
+ end
12
+ 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,10 @@
1
+ require_relative 'error_response'
2
+
3
+ module Yardi
4
+ module Error
5
+ # Raised when a Yardi response contains error message about not finding
6
+ # any guests.
7
+ class GuestsNotFound < ErrorResponse
8
+ end
9
+ end
10
+ 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,10 @@
1
+ require_relative 'base'
2
+
3
+ module Yardi
4
+ module Error
5
+ # Raised when the property we're trying to insert a guest card for is not
6
+ # in Yardi's system.
7
+ class MissingProperty < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'base'
2
+
3
+ module Yardi
4
+ module Error
5
+ # Raised when we get a successful Yardi response but no results e.g.
6
+ # GetResidents returns no <Resident> nodes.
7
+ class NoResults < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'error_response'
2
+
3
+ module Yardi
4
+ module Error
5
+ # Raised when Yardi responds with a 404
6
+ class ResourceNotFound < Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module Yardi
4
+ module Error
5
+ ##
6
+ # An Error that is raised when Yardi API is unavailable.
7
+ #
8
+ class ServiceUnavailable < Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module Yardi
4
+ module Error
5
+ ##
6
+ # An Error that is raised when Yardi response cannot be parsed.
7
+ #
8
+ class UnparsableResponse < 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,12 @@
1
+ module Yardi
2
+ module Model
3
+ class GuestCardResponse
4
+ attr_reader :messages, :remote_id
5
+
6
+ def initialize(messages:, remote_id:)
7
+ @messages = messages
8
+ @remote_id = remote_id
9
+ end
10
+ end
11
+ end
12
+ 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