yardi 4.0.8 → 4.11.2

Sign up to get free protection for your applications and to get access to all the features.
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 +28 -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 +30 -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.2'
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