real_page 2.3.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 (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