real_page 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.ci +139 -0
  3. data/.gitignore +3 -0
  4. data/.rakeTasks +7 -0
  5. data/.rspec +3 -0
  6. data/CHANGELOG.txt +12 -0
  7. data/CODEOWNERS +1 -0
  8. data/CODE_OF_CONDUCT.md +13 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +588 -0
  12. data/Rakefile +7 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +7 -0
  15. data/config/multi_xml.rb +4 -0
  16. data/lib/real_page.rb +54 -0
  17. data/lib/real_page/attribute_parser.rb +48 -0
  18. data/lib/real_page/attribute_parser/base.rb +21 -0
  19. data/lib/real_page/attribute_parser/boolean.rb +25 -0
  20. data/lib/real_page/attribute_parser/date.rb +28 -0
  21. data/lib/real_page/attribute_parser/date_time.rb +23 -0
  22. data/lib/real_page/attribute_parser/decimal.rb +15 -0
  23. data/lib/real_page/attribute_parser/integer.rb +15 -0
  24. data/lib/real_page/attribute_parser/object.rb +13 -0
  25. data/lib/real_page/attribute_parser/string.rb +13 -0
  26. data/lib/real_page/document_parser.rb +6 -0
  27. data/lib/real_page/document_parser/base.rb +51 -0
  28. data/lib/real_page/document_parser/floor_plan_object.rb +30 -0
  29. data/lib/real_page/document_parser/guest_cards.rb +102 -0
  30. data/lib/real_page/document_parser/guest_cards/amenities.rb +34 -0
  31. data/lib/real_page/document_parser/guest_cards/prospects.rb +45 -0
  32. data/lib/real_page/document_parser/leases.rb +37 -0
  33. data/lib/real_page/document_parser/picklist.rb +38 -0
  34. data/lib/real_page/document_parser/rent_matrices.rb +39 -0
  35. data/lib/real_page/document_parser/rent_matrices/options.rb +31 -0
  36. data/lib/real_page/document_parser/rent_matrices/rows.rb +30 -0
  37. data/lib/real_page/document_parser/unit_object.rb +30 -0
  38. data/lib/real_page/error/bad_request.rb +18 -0
  39. data/lib/real_page/error/base.rb +9 -0
  40. data/lib/real_page/error/invalid_configuration.rb +9 -0
  41. data/lib/real_page/error/invalid_response.rb +9 -0
  42. data/lib/real_page/error/request_fault.rb +19 -0
  43. data/lib/real_page/error/request_fault/details.rb +17 -0
  44. data/lib/real_page/model/activity.rb +39 -0
  45. data/lib/real_page/model/address.rb +17 -0
  46. data/lib/real_page/model/amenity.rb +14 -0
  47. data/lib/real_page/model/appointment.rb +23 -0
  48. data/lib/real_page/model/base.rb +63 -0
  49. data/lib/real_page/model/base/attribute.rb +56 -0
  50. data/lib/real_page/model/base/attribute_store.rb +37 -0
  51. data/lib/real_page/model/floor_plan.rb +33 -0
  52. data/lib/real_page/model/follow_up.rb +34 -0
  53. data/lib/real_page/model/guest_card.rb +49 -0
  54. data/lib/real_page/model/lease.rb +36 -0
  55. data/lib/real_page/model/lease_action.rb +32 -0
  56. data/lib/real_page/model/phone_number.rb +13 -0
  57. data/lib/real_page/model/picklist_item.rb +10 -0
  58. data/lib/real_page/model/preferences.rb +25 -0
  59. data/lib/real_page/model/prospect.rb +35 -0
  60. data/lib/real_page/model/quote.rb +56 -0
  61. data/lib/real_page/model/rent_matrix.rb +16 -0
  62. data/lib/real_page/model/rent_matrix/concessions.rb +15 -0
  63. data/lib/real_page/model/rent_matrix/option.rb +19 -0
  64. data/lib/real_page/model/rent_matrix/row.rb +18 -0
  65. data/lib/real_page/model/screening.rb +22 -0
  66. data/lib/real_page/model/unit.rb +61 -0
  67. data/lib/real_page/model/unit_shown.rb +48 -0
  68. data/lib/real_page/parameter/list_criterion.rb +14 -0
  69. data/lib/real_page/request/base.rb +89 -0
  70. data/lib/real_page/request/get_floor_plan_list.rb +45 -0
  71. data/lib/real_page/request/get_leases_by_traffic_source.rb +59 -0
  72. data/lib/real_page/request/get_marketing_sources_by_property.rb +23 -0
  73. data/lib/real_page/request/get_rent_matrix.rb +57 -0
  74. data/lib/real_page/request/get_units_by_property.rb +39 -0
  75. data/lib/real_page/request/prospect_search.rb +50 -0
  76. data/lib/real_page/request_section.rb +6 -0
  77. data/lib/real_page/request_section/auth.rb +31 -0
  78. data/lib/real_page/request_section/get_rent_matrix.rb +32 -0
  79. data/lib/real_page/request_section/list_criteria.rb +29 -0
  80. data/lib/real_page/request_section/parameter.rb +31 -0
  81. data/lib/real_page/request_section/prospect_search_criterion.rb +24 -0
  82. data/lib/real_page/utils.rb +6 -0
  83. data/lib/real_page/utils/array_fetcher.rb +35 -0
  84. data/lib/real_page/utils/configuration_validator.rb +20 -0
  85. data/lib/real_page/utils/request_fetcher.rb +30 -0
  86. data/lib/real_page/utils/request_generator.rb +52 -0
  87. data/lib/real_page/utils/snowflake_event_tracker.rb +107 -0
  88. data/lib/real_page/validator.rb +6 -0
  89. data/lib/real_page/validator/move_in_report.rb +65 -0
  90. data/lib/real_page/validator/prospects_data.rb +93 -0
  91. data/lib/real_page/validator/request_errors.rb +97 -0
  92. data/lib/real_page/validator/request_fault.rb +97 -0
  93. data/lib/real_page/version.rb +3 -0
  94. data/real_page.gemspec +32 -0
  95. metadata +291 -0
@@ -0,0 +1,30 @@
1
+ require 'faraday'
2
+
3
+ require 'real_page/utils'
4
+ require 'real_page/utils/configuration_validator'
5
+
6
+ module RealPage
7
+ module Utils
8
+ # Send a SOAP request to RealPage
9
+ class RequestFetcher
10
+ # @param generator [RequestGenerator] an instance of a RequestGenerator,
11
+ # which responds to #generate(pmc_id, site_id).
12
+ def initialize(generator:)
13
+ @generator = generator
14
+ end
15
+
16
+ # @return [String] the XML response from RealPage
17
+ def fetch
18
+ ConfigurationValidator.new.validate!
19
+ Faraday.new.post(RealPage.config.web_service_url) do |request|
20
+ request.body = generator.body
21
+ request.headers = generator.headers
22
+ end.body
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :generator
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ require 'nokogiri'
2
+
3
+ require 'real_page/utils'
4
+
5
+ module RealPage
6
+ module Utils
7
+ # Generate a SOAP request for a specific action and sections
8
+ class RequestGenerator
9
+ ENVELOPE = {
10
+ 'xmlns:soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/',
11
+ 'xmlns:tem' => 'http://tempuri.org/'
12
+ }.freeze
13
+
14
+ # @param soap_action [String] the action to request from RealPage.
15
+ # @param sections [Array<RequestSection>] the section generators that will
16
+ # be used to generate the body of the XML request
17
+ def initialize(soap_action, sections)
18
+ @soap_action = soap_action
19
+ @sections = sections
20
+ end
21
+
22
+ # @return [String] the XML request for the specified action and sections
23
+ def body
24
+ Nokogiri::XML::Builder.new do |xml_builder|
25
+ xml_builder.Envelope(ENVELOPE) do
26
+ namespace = xml_builder.parent.namespace_definitions.first
27
+ xml_builder.parent.namespace = namespace
28
+ xml_builder['soapenv'].Header
29
+ xml_builder['soapenv'].Body do
30
+ xml_builder['tem'].send(soap_action.downcase) do
31
+ sections.each do |section|
32
+ section.generate(xml_builder['tem'])
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end.to_xml
38
+ end
39
+
40
+ def headers
41
+ {
42
+ 'Content-Type' => 'text/xml; charset=utf-8',
43
+ 'SOAPAction' => "http://tempuri.org/IRPXService/#{soap_action.downcase}"
44
+ }
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :soap_action, :sections
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,107 @@
1
+ require 'securerandom'
2
+ require 'event_tracker'
3
+ require 'real_page/utils'
4
+
5
+ module RealPage
6
+ module Utils
7
+ class SnowflakeEventTracker
8
+ IMPORT_PMS_RESIDENT_EVENT = 'import_pms_resident'
9
+ IMPORT_PMS_PROSPECT_EVENT = 'import_pms_prospect'
10
+
11
+ def self.track_pms_resident_event(
12
+ remote_lease_id: nil,
13
+ resident_type:,
14
+ request_params:,
15
+ move_in_date: nil,
16
+ lease_to: nil,
17
+ lease_from: nil,
18
+ first_name_present: false,
19
+ last_name_present: false,
20
+ email_present: false,
21
+ phones_count:,
22
+ error: nil
23
+ )
24
+ EventTracker.track_process_events(name: IMPORT_PMS_RESIDENT_EVENT) do |events|
25
+ events.add_imported_event(
26
+ EventTracker::ResourceFactory.build_pms_resident(
27
+ billing_import: EventTracker::BillingImportFactory.build_billing_import(
28
+ property_id: request_params[:billing_config].property_id,
29
+ billing_config_id: request_params[:billing_config].id,
30
+ remote_id: request_params[:site_id],
31
+ pms_type: 'real_page',
32
+ import_id: request_params[:import_id],
33
+ pmc_id: request_params[:pmc_id],
34
+ service: RealPage.config.app_name
35
+ ),
36
+ remote_lease_id: remote_lease_id,
37
+ import_resident_id: import_resident_id(request_params[:import_id]),
38
+ resident_type: resident_type,
39
+ api_name: 'GetLeasesByTrafficSource',
40
+ request_params: EventTracker::ResourceFactory::PmsResident.build_request_params(
41
+ start_date: request_params[:start_date],
42
+ end_date: request_params[:end_date],
43
+ prospect_id: request_params[:prospect_id],
44
+ pmc_id: request_params[:pmc_id],
45
+ remote_id: request_params[:site_id],
46
+ traffic_source_id: request_params[:traffic_source_id]
47
+ ),
48
+ move_in_date: move_in_date,
49
+ lease_to: lease_to,
50
+ lease_from: lease_from,
51
+ first_name_present: first_name_present,
52
+ last_name_present: last_name_present,
53
+ email_present: email_present,
54
+ phones_count: phones_count,
55
+ error: error
56
+ )
57
+ )
58
+ end
59
+ end
60
+
61
+ def self.track_pms_prospect_event(
62
+ remote_lease_id: nil,
63
+ resident_type:,
64
+ request_params:,
65
+ contact_date: nil,
66
+ contact_source: nil,
67
+ remote_prospect_id: nil,
68
+ error: nil
69
+ )
70
+ EventTracker.track_process_events(name: IMPORT_PMS_PROSPECT_EVENT) do |events|
71
+ events.add_imported_event(
72
+ EventTracker::ResourceFactory.build_pms_prospect(
73
+ billing_import: EventTracker::BillingImportFactory.build_billing_import(
74
+ property_id: request_params[:billing_config].property_id,
75
+ billing_config_id: request_params[:billing_config].id,
76
+ remote_id: request_params[:site_id],
77
+ pms_type: 'real_page',
78
+ import_id: request_params[:import_id],
79
+ pmc_id: request_params[:pmc_id],
80
+ service: RealPage.config.app_name
81
+ ),
82
+ remote_lease_id: remote_lease_id,
83
+ import_resident_id: import_resident_id(request_params[:import_id]),
84
+ resident_type: resident_type,
85
+ api_name: 'ProspectSearch',
86
+ request_params: EventTracker::ResourceFactory::PmsProspect.build_request_params(
87
+ pmc_id: request_params[:pmc_id],
88
+ remote_id: request_params[:site_id],
89
+ prospect_id: request_params[:guest_card_id]
90
+ ),
91
+ contact_date: contact_date,
92
+ contact_source: contact_source,
93
+ remote_prospect_id: remote_prospect_id,
94
+ error: error
95
+ )
96
+ )
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def self.import_resident_id(import_id)
103
+ "#{import_id}-#{SecureRandom.alphanumeric(15)}"
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,6 @@
1
+ module RealPage
2
+ module Validator
3
+ end
4
+
5
+ private_constant :Validator
6
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/core_ext/object'
2
+
3
+ require 'real_page/validator'
4
+ require 'real_page/utils/snowflake_event_tracker'
5
+
6
+ module RealPage
7
+ module Validator
8
+ # Send parased response's move-in report data to Snowflake via event tracker
9
+
10
+ class MoveInReport
11
+ def initialize(response, request_params, request_name)
12
+ @response = response
13
+ @request_params = request_params
14
+ end
15
+
16
+ def validate!
17
+ return if leases.blank?
18
+ send_successful_response_data_to_snowflake
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :response, :request_params
24
+
25
+ def send_successful_response_data_to_snowflake
26
+ leases.each do |lease|
27
+ Utils::SnowflakeEventTracker.track_pms_resident_event(
28
+ remote_lease_id: lease['leaseid'],
29
+ resident_type: 'PRIMARY',
30
+ request_params: request_params,
31
+ move_in_date: DateTime.strptime(lease['moveindate'], '%m/%d/%y').to_time,
32
+ lease_to: DateTime.strptime(lease['leaseenddate'], '%m/%d/%y').to_time,
33
+ lease_from: DateTime.strptime(lease['leasebegindate'], '%m/%d/%y').to_time,
34
+ first_name_present: !lease['firstname'].blank?,
35
+ last_name_present: !lease['lastname'].blank?,
36
+ email_present: !lease['email'].blank?,
37
+ phones_count: phones_count(lease)
38
+ )
39
+ end
40
+ end
41
+
42
+ def leases
43
+ body = response['s:Envelope']['s:Body']
44
+ response_key = body.keys.detect { |key| key !~ /^xmlns/ }
45
+ contents_response = body.fetch(response_key)
46
+ result_key = contents_response.keys.detect { |key| key !~ /^xmlns/ }
47
+ contents_result = contents_response.fetch(result_key)
48
+ result = contents_result.values.first
49
+ leases_hash = result.dig('leases', 'lease')
50
+ return [] if leases_hash.nil?
51
+ return [leases_hash] unless leases_hash.is_a?(Array)
52
+ leases_hash
53
+ end
54
+
55
+ def phones_count(lease)
56
+ count = 0
57
+ phone_keys = lease.keys.select { |key| key.include?('phone') && !key.include?('phoneext') }
58
+ phone_keys.each do |phone_key|
59
+ count +=1 unless lease[phone_key].blank?
60
+ end
61
+ count
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,93 @@
1
+ require 'active_support/core_ext/object'
2
+
3
+ require 'real_page/validator'
4
+ require 'real_page/utils/snowflake_event_tracker'
5
+
6
+ module RealPage
7
+ module Validator
8
+ # Sends parased response's roommate and prospect data to Snowflake via event tracker
9
+
10
+ class ProspectsData
11
+ def initialize(response, request_params, request_name)
12
+ @response = response
13
+ @request_params = request_params
14
+ @guest_cards = guest_cards
15
+ end
16
+
17
+ def validate!
18
+ return if guest_cards.blank?
19
+ send_data_to_snowflake
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :response, :request_params, :guest_cards
25
+
26
+ def send_data_to_snowflake
27
+ guest_cards.each do |guest_card|
28
+ prospects_of_current_guest_card = prospects(guest_card)
29
+ send_roommates_data_to_snowflake(prospects_of_current_guest_card)
30
+ send_prospects_data_to_snowflake(prospects_of_current_guest_card, guest_card)
31
+ end
32
+ end
33
+
34
+ def send_roommates_data_to_snowflake(prospects)
35
+ prospects.each do |prospect|
36
+ Utils::SnowflakeEventTracker.track_pms_resident_event(
37
+ resident_type: 'ROOMMATE',
38
+ request_params: request_params,
39
+ first_name_present: !prospect['FirstName'].blank?,
40
+ last_name_present: !prospect['LastName'].blank?,
41
+ email_present: !prospect['Email'].blank?,
42
+ phones_count: phones_count(prospect)
43
+ )
44
+ end
45
+ end
46
+
47
+ def send_prospects_data_to_snowflake(prospects, guest_card)
48
+ prospects.each do |prospect|
49
+ Utils::SnowflakeEventTracker.track_pms_prospect_event(
50
+ resident_type: 'ROOMMATE',
51
+ request_params: request_params,
52
+ contact_date: DateTime.parse(guest_card['DateContact']).to_time,
53
+ contact_source: guest_card['PrimaryLeadSource'],
54
+ remote_prospect_id: prospect['IdNumber']
55
+ )
56
+ end
57
+ end
58
+
59
+ def guest_cards
60
+ guest_cards_hash = parsed_response.dig('GuestCards', 'GuestCard')
61
+ return [] if guest_cards_hash.nil?
62
+ return [guest_cards_hash] unless guest_cards_hash.is_a?(Array)
63
+ guest_cards_hash
64
+ end
65
+
66
+ def prospects(guest_card)
67
+ prospects_hash = guest_card.dig('Prospects', 'Prospect')
68
+ return [] if prospects_hash.nil?
69
+ return [prospects_hash] unless prospects_hash.is_a?(Array)
70
+ prospects_hash
71
+ end
72
+
73
+ def parsed_response
74
+ body = response['s:Envelope']['s:Body']
75
+ response_key = body.keys.detect { |key| key !~ /^xmlns/ }
76
+ contents_response = body.fetch(response_key)
77
+ result_key = contents_response.keys.detect { |key| key !~ /^xmlns/ }
78
+ contents_result = contents_response.fetch(result_key)
79
+ result = contents_result.values.first
80
+ end
81
+
82
+ def phones_count(prospect)
83
+ phone_numbers_hash = prospect.dig('Numbers', 'PhoneNumber')
84
+ return 0 if phone_numbers_hash.blank?
85
+ if !phone_numbers_hash.is_a?(Array)
86
+ return 0 if phone_numbers_hash['Number'].blank?
87
+ return 1
88
+ end
89
+ phone_numbers_hash.select { |phone_number| !phone_number['Number'].blank? }.length
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,97 @@
1
+ require 'active_support/core_ext/object'
2
+
3
+ require 'real_page/utils/array_fetcher'
4
+ require 'real_page/utils/snowflake_event_tracker'
5
+
6
+ module RealPage
7
+ module Validator
8
+ # Ensure there are no errors in the wrapped contents of the body.
9
+ #
10
+ # This validator works on responses that have the following format:
11
+ #
12
+ # <s:Envelope>
13
+ # <s:Body>
14
+ # <actionnameResponse>
15
+ # <actionnameResult>
16
+ # <actionnameresponse>
17
+ # <success>false</success>
18
+ # <errorcount>1</errorcount>
19
+ # <errors>
20
+ # <error>
21
+ # <severity>Critical</severity>
22
+ # <errornumber>1</errornumber>
23
+ # <errormessage>something went wrong</errormessage>
24
+ # <internalerrormessage/>
25
+ # </error>
26
+ # </errors>
27
+ # </actionnameresponse>
28
+ # </actionnameResult>
29
+ # </actionnameResponse>
30
+ # </s:Body>
31
+ # </s:Envelope>
32
+ class RequestErrors
33
+ # Ensure the concatenated error message does not exceed Snowflake error column max
34
+ # character length. The error column data type is VARCHAR(16777216)
35
+ # 16777216 bytes / 4 bytes per character = 4194304 characters
36
+ MAX_ERROR_LENGTH = 4194304
37
+ private_constant :MAX_ERROR_LENGTH
38
+
39
+ # @param response [Hash<String, Object>] the XML response parsed into a
40
+ # Hash
41
+ def initialize(response, request_params, request_name)
42
+ @response = response
43
+ @request_params = request_params
44
+ end
45
+
46
+ # @raise [RealPage::Error::RequestFault] if the response has an error
47
+ # node in the contents
48
+ def validate!
49
+ return unless error?
50
+ send_request_error_to_snowflake
51
+ raise RealPage::Error::BadRequest.new(errors)
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :response, :request_params
57
+
58
+ def contents
59
+ body = response['s:Envelope']['s:Body']
60
+ response_key = body.keys.detect { |key| key !~ /^xmlns/ }
61
+ contents_response = body[response_key]
62
+ result_key = contents_response.keys.detect { |key| key !~ /^xmlns/ }
63
+ contents_result = contents_response[result_key]
64
+ contents_result.values.first
65
+ end
66
+
67
+ def error?
68
+ contents['success'] == 'false'
69
+ end
70
+
71
+ def errors
72
+ errors = contents['errors']
73
+ errors_array = Utils::ArrayFetcher.new(hash: errors, key: 'error').fetch
74
+ errors_array.map do |error|
75
+ Struct.new(:message, :severity, :internal_message).new(
76
+ error['errormessage'],
77
+ error['severity'],
78
+ error['internalerrormessage']
79
+ )
80
+ end
81
+ end
82
+
83
+ def send_request_error_to_snowflake
84
+ Utils::SnowflakeEventTracker.track_pms_resident_event(
85
+ resident_type: 'PRIMARY',
86
+ request_params: request_params,
87
+ phones_count: 0,
88
+ error: error_messages
89
+ )
90
+ end
91
+
92
+ def error_messages
93
+ errors.map(&:message).join(' ').slice(0, MAX_ERROR_LENGTH)
94
+ end
95
+ end
96
+ end
97
+ end