yardi 4.0.8 → 4.11.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.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +6 -19
  4. data/.rubocop_todo.yml +264 -0
  5. data/Gemfile +11 -0
  6. data/README.md +50 -28
  7. data/bin/console +0 -0
  8. data/lib/yardi/document_parser/base.rb +35 -5
  9. data/lib/yardi/document_parser/guest_card_import_response_object.rb +1 -1
  10. data/lib/yardi/document_parser/properties.rb +39 -0
  11. data/lib/yardi/document_parser/prospects.rb +23 -5
  12. data/lib/yardi/document_parser/residents.rb +8 -4
  13. data/lib/yardi/error/canceled_guest.rb +9 -0
  14. data/lib/yardi/model/property.rb +27 -0
  15. data/lib/yardi/model/resident.rb +2 -1
  16. data/lib/yardi/parameter/contact_info.rb +1 -1
  17. data/lib/yardi/parameter/prospect.rb +3 -1
  18. data/lib/yardi/parameter/user.rb +1 -2
  19. data/lib/yardi/request/base.rb +27 -5
  20. data/lib/yardi/request/get_property_configurations.rb +36 -0
  21. data/lib/yardi/request/get_residents.rb +3 -2
  22. data/lib/yardi/request/get_yardi_guest_activity.rb +11 -6
  23. data/lib/yardi/request/import_yardi_guest.rb +18 -3
  24. data/lib/yardi/request_section/lead_management.rb +72 -9
  25. data/lib/yardi/request_section/prospect.rb +1 -0
  26. data/lib/yardi/utils/google_cloud_storage.rb +25 -0
  27. data/lib/yardi/utils/request_fetcher.rb +3 -5
  28. data/lib/yardi/utils/request_generator.rb +1 -0
  29. data/lib/yardi/utils/snowflake_event_tracker.rb +107 -0
  30. data/lib/yardi/utils/test_data_fetcher.rb +27 -0
  31. data/lib/yardi/validator/base.rb +114 -0
  32. data/lib/yardi/validator/empty_response.rb +25 -8
  33. data/lib/yardi/validator/error_response.rb +31 -8
  34. data/lib/yardi/validator/fault_response.rb +28 -6
  35. data/lib/yardi/validator/missing_property.rb +20 -4
  36. data/lib/yardi/validator/prospect_eventing.rb +42 -0
  37. data/lib/yardi/validator/resident_eventing.rb +45 -0
  38. data/lib/yardi/version.rb +1 -1
  39. data/lib/yardi.rb +7 -2
  40. data/yardi.gemspec +6 -1
  41. metadata +78 -12
@@ -0,0 +1,114 @@
1
+ require 'yardi/validator'
2
+ require 'yardi/utils/snowflake_event_tracker'
3
+ require 'yardi/utils/phone_parser'
4
+ require 'securerandom'
5
+ require 'active_support'
6
+ require 'active_support/core_ext'
7
+
8
+ module Yardi
9
+ module Validator
10
+ class Base
11
+ def send_pms_resident_event(lease:, resident_type:, params:, error_message: nil)
12
+ if lease
13
+ remote_lease_id = lease['tCode']
14
+ import_resident_id = lease['import_resident_id']
15
+ move_in_date = parse_date(lease['MoveInDate'])
16
+ lease_to = parse_date(lease['LeaseToDate'])
17
+ lease_from = parse_date(lease['LeaseFromDate'])
18
+ first_name_present = lease['FirstName'].present?
19
+ last_name_present = lease['LastName'].present?
20
+ email_present = lease['Email'].present?
21
+ phones_count = (Utils::PhoneParser.parse(lease['Phone']) || []).length
22
+ unit_name = lease['UnitCode'] || ''
23
+ resident_id = lease['tCode']
24
+ else
25
+ remote_lease_id, move_in_date, lease_to, lease_from = [nil] * 4
26
+ import_resident_id = params[:import_id]
27
+ first_name_present, last_name_present, email_present = [false] * 3
28
+ phones_count = 0
29
+ unit_name = ''
30
+ resident_id = nil
31
+ end
32
+ Utils::SnowflakeEventTracker.track_pms_resident_event(
33
+ remote_lease_id: remote_lease_id,
34
+ import_resident_id: import_resident_id,
35
+ resident_type: resident_type,
36
+ request_params: pms_resident_request_params(params),
37
+ move_in_date: move_in_date,
38
+ lease_to: lease_to,
39
+ lease_from: lease_from,
40
+ first_name_present: first_name_present,
41
+ last_name_present: last_name_present,
42
+ email_present: email_present,
43
+ phones_count: phones_count,
44
+ unit_name: unit_name,
45
+ resident_id: resident_id,
46
+ error: error_message
47
+ )
48
+ end
49
+
50
+ def pms_resident_request_params(params)
51
+ {
52
+ start_date: params[:start_date]&.to_time,
53
+ end_date: params[:end_date]&.to_time,
54
+ prospect_id: nil,
55
+ pmc_id: params[:pmc_id],
56
+ remote_id: params[:property_id],
57
+ traffic_source_id: nil,
58
+ import_id: params[:import_id],
59
+ billing_config_id: params[:billing_config]&.id,
60
+ property_id: params[:billing_config]&.property_id
61
+ }
62
+ end
63
+
64
+ def send_pms_prospect_event(params:, event: nil, error_message: nil)
65
+ if event
66
+ contact_date = event['EventDate']&.to_time
67
+ contact_source = event['TransactionSource']
68
+ else
69
+ contact_date, contact_source = [nil] * 2
70
+ end
71
+ Utils::SnowflakeEventTracker.track_pms_prospect_event(
72
+ remote_lease_id: nil,
73
+ request_params: pms_prospect_request_params(params),
74
+ first_name_present: params[:prospect]&.first_name.present?,
75
+ last_name_present: params[:prospect]&.last_name.present?,
76
+ email_present: params[:prospect]&.email.present?,
77
+ phone_present: params[:prospect]&.phone.present?,
78
+ contact_date: contact_date,
79
+ contact_source: contact_source,
80
+ remote_prospect_id: params[:prospect]&.yardi_prospect_id,
81
+ error: error_message
82
+ )
83
+ end
84
+
85
+ def pms_prospect_request_params(params)
86
+ {
87
+ property_id: params[:billing_config]&.property_id,
88
+ billing_config_id: params[:billing_config]&.id,
89
+ remote_id: params[:property_id],
90
+ import_id: params[:import_id],
91
+ pmc_id: params[:pmc_id],
92
+ import_resident_id: params[:import_resident_id],
93
+ prospect_id: params[:prospect]&.yardi_prospect_id
94
+ }
95
+ end
96
+
97
+ def parse_date(date)
98
+ (date && Date.strptime(date, '%m/%d/%Y'))&.to_time
99
+ end
100
+
101
+ def import_resident_id(params)
102
+ "#{params[:import_id]}-#{SecureRandom.alphanumeric(15)}"
103
+ end
104
+
105
+ def remote_id(customer, id_type)
106
+ desired_id_node = customer['Identification']&.detect do |id_node|
107
+ id_node['IDType'] == id_type
108
+ end
109
+
110
+ desired_id_node['IDValue']
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,41 +1,58 @@
1
1
  require 'yardi/validator'
2
+ require_relative 'base'
2
3
 
3
4
  module Yardi
4
5
  module Validator
5
6
  # Ensure that the response contains data. Sometimes Yardi will respond with
6
7
  # just the outer shell of XML. For an example, @see empty_response.xml.
7
8
  # We will also raise an error if Yardi returns a completely empty response.
8
- class EmptyResponse
9
+ class EmptyResponse < Base
9
10
  # @param parsed_response [Hash<String, Object>] the XML response parsed
10
11
  # into a Hash
11
12
  # @param action [String] The SOAP action this response is for. Yardi's
12
13
  # responses have nodes whose names include the SOAP action for the
13
14
  # request that was made.
14
- def initialize(action:, parsed_response:)
15
+ def initialize(action:, parsed_response:, params:)
15
16
  @action = action
16
17
  @response = parsed_response
18
+ @params = params
17
19
  end
18
20
 
19
21
  # @raise [Yardi::Error::EmptyResponse] if the response is effectively
20
22
  # empty
21
23
  def validate!
22
24
  return unless error?
23
- raise Error::EmptyResponse, 'Yardi response contains no Result node.'
25
+
26
+ message = 'Yardi response contains no Result node.'
27
+
28
+ case action
29
+ when 'GetResidents'
30
+ send_pms_resident_event(lease: nil, resident_type: 'PRIMARY', params: params, error_message: message)
31
+ when 'GetYardiGuestActivity_Search'
32
+ send_pms_prospect_event(params: params, error_message: message)
33
+ end
34
+ raise Error::EmptyResponse, message
24
35
  end
25
36
 
26
37
  private
27
38
 
28
- attr_reader :action, :response
39
+ attr_reader :action, :response, :params
29
40
 
30
41
  def error?
31
42
  return true if response.empty?
32
43
 
33
- envelope = response['soap:Envelope']
44
+ envelope = response['soap:Envelope'] || response['Envelope']
34
45
  return true if envelope.nil?
35
46
 
36
- body = envelope['soap:Body']
37
- # Fault responses will be handled by the FaultResponse validator
38
- return false unless body['soap:Fault'].nil?
47
+ if envelope['soap:Body']
48
+ body = envelope['soap:Body']
49
+ # Fault responses will be handled by the FaultResponse validator
50
+ return false unless body['soap:Fault'].nil?
51
+ else
52
+ body = envelope['Body']
53
+ # Fault responses will be handled by the FaultResponse validator
54
+ return false unless body['Fault'].nil?
55
+ end
39
56
  body["#{action}Response"]["#{action}Result"].nil?
40
57
  end
41
58
  end
@@ -1,31 +1,47 @@
1
1
  require 'yardi/validator'
2
+ require_relative 'base'
2
3
 
3
4
  module Yardi
4
5
  module Validator
5
6
  # Ensure that the response has no Messages with messageType=Error.
6
- class ErrorResponse
7
+ class ErrorResponse < Base
7
8
  # @param parsed_response [Hash<String, Object>] XML response parsed into a
8
9
  # Hash
9
- def initialize(action:, parsed_response:)
10
+ def initialize(action:, parsed_response:, params:)
10
11
  @action = action
11
12
  @response = parsed_response
13
+ @params = params
12
14
  end
13
15
 
14
16
  # @raise [Yardi::Error::ErrorResponse] if the response has an error
15
17
  def validate!
16
18
  return unless error?
17
19
 
18
- guest_err_msg = "Error: No guests exist with the given search criteria."
20
+ case action
21
+ when 'GetResidents'
22
+ send_pms_resident_event(lease: nil, resident_type: 'PRIMARY', params: params,
23
+ error_message: error_messages.join('. '))
24
+ when 'GetYardiGuestActivity_Search'
25
+ if params[:send_prospect_events] != false
26
+ send_pms_prospect_event(params: params, error_message: error_messages.join('. '))
27
+ end
28
+ end
19
29
 
20
- if error_messages.include?(guest_err_msg)
21
- raise Error::GuestsNotFound
30
+ canceled_guest_err_msg = "Status is 'Canceled Guest', Prospect must be reactivated (ReActivate event)"
31
+ error_messages.each do |error_message|
32
+ raise Error::CanceledGuest, error_messages.join('. ') if error_message.include?(canceled_guest_err_msg)
22
33
  end
34
+
35
+ guest_err_msg = 'Error: No guests exist with the given search criteria.'
36
+
37
+ raise Error::GuestsNotFound, error_messages.join('. ') if error_messages.include?(guest_err_msg)
38
+
23
39
  raise Error::ErrorResponse, error_messages.join('. ')
24
40
  end
25
41
 
26
42
  private
27
43
 
28
- attr_reader :action, :response
44
+ attr_reader :action, :response, :params
29
45
 
30
46
  def error?
31
47
  messages.any? do |message_node|
@@ -51,16 +67,23 @@ module Yardi
51
67
  # messages so that we don't have to worry about type checks when pulling
52
68
  # out the errors.
53
69
  def messages
54
- body = response['soap:Envelope']['soap:Body']
70
+ body = if response['soap:Envelope']
71
+ response['soap:Envelope']['soap:Body']
72
+ else
73
+ response['Envelope']['Body']
74
+ end
75
+
55
76
  # This will be picked up by either the EmptyResponse or the
56
77
  # FaultResponse check. Whatever it is, it's not an ErrorResponse issue.
57
78
  return [] if body["#{action}Response"].nil?
79
+
58
80
  result = body["#{action}Response"]["#{action}Result"]
59
81
  # There are no Messages nodes in a successful
60
82
  # GetYardiGuestActivity_Search response
61
83
  return [] if result['Messages'].nil?
84
+
62
85
  nodes = result['Messages']['Message']
63
- nodes = nodes.is_a?(Array) ? nodes : [nodes]
86
+ nodes = [nodes] unless nodes.is_a?(Array)
64
87
  # see method comment for why this normalization is needed
65
88
  nodes.map do |node|
66
89
  if node.is_a?(Hash)
@@ -1,36 +1,58 @@
1
1
  require 'yardi/validator'
2
+ require_relative 'base'
2
3
 
3
4
  module Yardi
4
5
  module Validator
5
6
  # Ensure that the response has no Fault nodes
6
- class FaultResponse
7
+ class FaultResponse < Base
7
8
  # @param parsed_response [Hash<String, Object>] XML response parsed into a
8
9
  # Hash
9
- def initialize(action:, parsed_response:)
10
+ def initialize(action:, parsed_response:, params:)
10
11
  @action = action
11
12
  @response = parsed_response
13
+ @params = params
12
14
  end
13
15
 
14
16
  # @raise [Yardi::Error::FaultResponse] if the response has a Fault node
15
17
  def validate!
16
18
  return unless error?
19
+
20
+ case action
21
+ when 'GetResidents'
22
+ send_pms_resident_event(lease: nil, resident_type: 'PRIMARY', params: params, error_message: error_message)
23
+ when 'GetYardiGuestActivity_Search'
24
+ send_pms_prospect_event(params: params, error_message: error_message)
25
+ end
17
26
  raise Error::FaultResponse, error_message
18
27
  end
19
28
 
20
29
  private
21
30
 
22
- attr_reader :action, :response
31
+ attr_reader :action, :response, :params
23
32
 
24
33
  def body
25
- response['soap:Envelope']['soap:Body']
34
+ if response['soap:Envelope']
35
+ response['soap:Envelope']['soap:Body']
36
+ else
37
+ response['Envelope']['Body']
38
+ end
26
39
  end
27
40
 
28
41
  def error?
29
- !body['soap:Fault'].nil?
42
+ if response['soap:Envelope']
43
+ !body['soap:Fault'].nil?
44
+ else
45
+ !body['Fault'].nil?
46
+ end
30
47
  end
31
48
 
32
49
  def error_message
33
- fault = body['soap:Fault']
50
+ fault = if response['soap:Envelope']
51
+ body['soap:Fault']
52
+ else
53
+ body['Fault']
54
+ end
55
+
34
56
  fault_message = "#{fault['faultcode']}: #{fault['faultstring']}."
35
57
  details = fault['detail']
36
58
  details.nil? ? fault_message : "#{fault_message} Details: #{details}"
@@ -1,29 +1,38 @@
1
1
  require 'yardi/validator'
2
+ require_relative 'base'
2
3
 
3
4
  module Yardi
4
5
  module Validator
5
6
  # Make sure the property we're trying to send a guestcard for is configured
6
7
  # in Yardi's system. For an example, @see invalid_property.xml
7
- class MissingProperty
8
+ class MissingProperty < Base
8
9
  UNCONFIGURED_PROPERTY_REGEX = /Invalid Yardi Property Code/i
9
10
 
10
11
  # @param parsed_response [Hash<String, Object>] the XML response parsed
11
12
  # into a Hash
12
- def initialize(action:, parsed_response:)
13
+ def initialize(action:, parsed_response:, params:)
13
14
  @action = action
14
15
  @response = parsed_response
16
+ @params = params
15
17
  end
16
18
 
17
19
  # @raise [Yardi::Error::ErrorResponse] if the response is effectively
18
20
  # empty
19
21
  def validate!
20
22
  return if valid_property?
23
+
24
+ case action
25
+ when 'GetResidents'
26
+ send_pms_resident_event(lease: nil, resident_type: 'PRIMARY', params: params, error_message: message)
27
+ when 'GetYardiGuestActivity_Search'
28
+ send_pms_prospect_event(params: params, error_message: message)
29
+ end
21
30
  raise Error::MissingProperty, message
22
31
  end
23
32
 
24
33
  private
25
34
 
26
- attr_reader :action, :response
35
+ attr_reader :action, :response, :params
27
36
 
28
37
  def valid_property?
29
38
  message !~ UNCONFIGURED_PROPERTY_REGEX
@@ -44,14 +53,21 @@ module Yardi
44
53
  # we know it isn't an invalid property error so we return an empty string
45
54
  # which will fail the comparison to the regex.
46
55
  def message
47
- body = response['soap:Envelope']['soap:Body']
56
+ body = if response['soap:Envelope']
57
+ response['soap:Envelope']['soap:Body']
58
+ else
59
+ response['Envelope']['Body']
60
+ end
61
+
48
62
  # This will be picked up by either the EmptyResponse or the
49
63
  # FaultResponse check. Whatever it is, it's not a MissingProperty issue.
50
64
  return '' if body["#{action}Response"].nil?
65
+
51
66
  result = body["#{action}Response"]["#{action}Result"]
52
67
  # There are no Messages nodes in a successful
53
68
  # GetYardiGuestActivity_Search response
54
69
  return '' if result['Messages'].nil?
70
+
55
71
  messages = result['Messages']['Message']
56
72
  messages.is_a?(Hash) ? messages['__content__'] : ''
57
73
  end
@@ -0,0 +1,42 @@
1
+ require 'yardi/validator'
2
+ require_relative 'base'
3
+
4
+ module Yardi
5
+ module Validator
6
+ class ProspectEventing < Base
7
+ # Sends parased response's roommate and prospect data to Snowflake via event tracker
8
+ def initialize(action:, parsed_response:, params:)
9
+ @action = action
10
+ @response = parsed_response
11
+ @params = params
12
+ end
13
+
14
+ def validate!
15
+ return if response.nil?
16
+
17
+ send_prospect_events
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :action, :response, :params
23
+
24
+ def send_prospect_events
25
+ body = if response['soap:Envelope']
26
+ response['soap:Envelope']['soap:Body']
27
+ else
28
+ response['Envelope']['Body']
29
+ end
30
+
31
+ result_node = body["#{action}Response"]["#{action}Result"]
32
+ prospects = result_node['LeadManagement']['Prospects']['Prospect']
33
+ prospects = [prospects] unless prospects.is_a?(Array)
34
+ prospects.map do |prospect|
35
+ events = prospect.dig('Events', 'Event') || []
36
+ events_array = events.is_a?(Array) ? events : [events]
37
+ events_array.each { |event| send_pms_prospect_event(params: params, event: event) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ require 'yardi/validator'
2
+ require_relative 'base'
3
+
4
+ module Yardi
5
+ module Validator
6
+ class ResidentEventing < Base
7
+ attr_reader :action, :response, :params
8
+
9
+ # Send parased response's individual resident data to Snowflake via event tracker
10
+ def initialize(action:, parsed_response:, params:)
11
+ @action = action
12
+ @response = parsed_response
13
+ @params = params
14
+ end
15
+
16
+ # @raise [Yardi::Error::EmptyResponse] if the response is effectively
17
+ # empty
18
+ def validate!
19
+ return if response.nil?
20
+
21
+ body = if response['soap:Envelope']
22
+ response['soap:Envelope']['soap:Body']
23
+ else
24
+ response['Envelope']['Body']
25
+ end
26
+ result_node = body["#{action}Response"]["#{action}Result"]
27
+ path = %w[MITS_ResidentData PropertyResidents Residents Resident]
28
+ results = [result_node.dig(*path)].flatten.compact
29
+ results.map { |resident| send_all_events(resident) }
30
+ end
31
+
32
+ def send_all_events(resident)
33
+ resident['import_resident_id'] = import_resident_id(params)
34
+ send_pms_resident_event(lease: resident, resident_type: 'PRIMARY', params: params)
35
+ roommate_nodes = resident.dig('OtherOccupants', 'OtherOccupant')
36
+ unless roommate_nodes.nil?
37
+ [roommate_nodes].flatten.map do |roommate|
38
+ roommate['import_resident_id'] = import_resident_id(params)
39
+ send_pms_resident_event(lease: roommate, resident_type: 'ROOMMATE', params: params)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/yardi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Yardi
4
- VERSION = '4.0.8'
4
+ VERSION = '4.11.0'
5
5
  end
data/lib/yardi.rb CHANGED
@@ -1,7 +1,8 @@
1
- require_relative '../config/multi_xml.rb'
1
+ require_relative '../config/multi_xml'
2
2
 
3
3
  require 'yardi/error/base'
4
4
  require 'yardi/error/connection_error'
5
+ require 'yardi/error/canceled_guest'
5
6
  require 'yardi/error/empty_response'
6
7
  require 'yardi/error/error_response'
7
8
  require 'yardi/error/fault_response'
@@ -16,6 +17,7 @@ require 'yardi/error/unparsable_response'
16
17
  require 'yardi/model/event'
17
18
  require 'yardi/model/guest_card_response'
18
19
  require 'yardi/model/resident'
20
+ require 'yardi/model/property'
19
21
 
20
22
  require 'yardi/parameter/agent'
21
23
  require 'yardi/parameter/contact_info'
@@ -27,6 +29,7 @@ require 'yardi/parameter/user'
27
29
  require 'yardi/request/import_yardi_guest'
28
30
  require 'yardi/request/get_yardi_guest_activity'
29
31
  require 'yardi/request/get_residents'
32
+ require 'yardi/request/get_property_configurations'
30
33
 
31
34
  require 'yardi/version'
32
35
  # The toplevel Yardi module. This includes configuration information that
@@ -37,13 +40,15 @@ module Yardi
37
40
  entity
38
41
  license_key
39
42
  platform
43
+ app_name
44
+ gcs_bucket
40
45
  ].freeze
41
46
 
42
47
  Config = Struct.new(*CONFIG_KEYS)
43
48
  private_constant :Config
44
49
 
45
50
  class << self
46
- def configure(&block)
51
+ def configure
47
52
  yield config
48
53
  end
49
54
 
data/yardi.gemspec CHANGED
@@ -21,11 +21,16 @@ Gem::Specification.new do |spec|
21
21
  spec.add_runtime_dependency 'multi_xml', '~> 0.5' # Parse XML responses
22
22
  spec.add_runtime_dependency 'nokogiri', '~> 1.6' # Build XML for requests
23
23
  spec.add_runtime_dependency 'ox', '~> 2.3' # Parser we want with MultiXML
24
+ spec.add_runtime_dependency 'activesupport', '~> 5.2'
24
25
 
25
26
  spec.add_development_dependency 'bundler', '~> 1.10'
26
27
  spec.add_development_dependency 'pry-byebug', '~> 3.4'
27
28
  spec.add_development_dependency 'rake', '~> 11.2'
28
29
  spec.add_development_dependency 'rspec', '~> 3.4'
29
- spec.add_development_dependency 'rubocop', '~> 0.65'
30
30
  spec.add_development_dependency 'webmock', '~> 1.21'
31
+
32
+ spec.add_development_dependency 'rubocop', '= 1.23.0'
33
+ spec.add_development_dependency 'rubocop-git', '= 0.1.3'
34
+ spec.add_development_dependency 'rubocop-rails', '= 2.12.4'
35
+ spec.add_development_dependency 'rubocop-rspec', '= 2.6.0'
31
36
  end