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
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'pry'
5
+ require 'real_page'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ require 'multi_xml'
2
+ require 'ox'
3
+
4
+ MultiXml.parser = :ox
data/lib/real_page.rb ADDED
@@ -0,0 +1,54 @@
1
+ require_relative '../config/multi_xml.rb'
2
+
3
+ require 'real_page/error/bad_request'
4
+ require 'real_page/error/invalid_configuration'
5
+ require 'real_page/error/invalid_response'
6
+ require 'real_page/error/request_fault'
7
+
8
+ require 'real_page/model/activity'
9
+ require 'real_page/model/address'
10
+ require 'real_page/model/amenity'
11
+ require 'real_page/model/appointment'
12
+ require 'real_page/model/floor_plan'
13
+ require 'real_page/model/follow_up'
14
+ require 'real_page/model/guest_card'
15
+ require 'real_page/model/lease'
16
+ require 'real_page/model/lease_action'
17
+ require 'real_page/model/phone_number'
18
+ require 'real_page/model/picklist_item'
19
+ require 'real_page/model/preferences'
20
+ require 'real_page/model/prospect'
21
+ require 'real_page/model/quote'
22
+ require 'real_page/model/rent_matrix'
23
+ require 'real_page/model/rent_matrix/concessions'
24
+ require 'real_page/model/rent_matrix/option'
25
+ require 'real_page/model/rent_matrix/row'
26
+ require 'real_page/model/screening'
27
+ require 'real_page/model/unit'
28
+ require 'real_page/model/unit_shown'
29
+
30
+ require 'real_page/parameter/list_criterion'
31
+
32
+ require 'real_page/request/get_floor_plan_list'
33
+ require 'real_page/request/get_leases_by_traffic_source'
34
+ require 'real_page/request/get_marketing_sources_by_property'
35
+ require 'real_page/request/get_rent_matrix'
36
+ require 'real_page/request/get_units_by_property'
37
+ require 'real_page/request/prospect_search'
38
+
39
+ require 'real_page/version'
40
+
41
+ module RealPage
42
+ Config = Struct.new(:web_service_url, :username, :password, :license_key, :app_name)
43
+ private_constant :Config
44
+
45
+ class << self
46
+ def configure(&block)
47
+ yield config
48
+ end
49
+
50
+ def config
51
+ @config ||= Config.new
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ require 'real_page/attribute_parser/boolean'
2
+ require 'real_page/attribute_parser/date'
3
+ require 'real_page/attribute_parser/date_time'
4
+ require 'real_page/attribute_parser/decimal'
5
+ require 'real_page/attribute_parser/integer'
6
+ require 'real_page/attribute_parser/object'
7
+ require 'real_page/attribute_parser/string'
8
+
9
+ module RealPage
10
+ # Parse the string value from the XML response into the configured data type
11
+ class AttributeParser
12
+ # @param value [String] the response value from RealPage
13
+ # @param type [Symbol] the attribute's configured data type
14
+ def initialize(value:, type:)
15
+ @value = value
16
+ @type = type
17
+ end
18
+
19
+ # @return [Object] the parsed attribute value
20
+ def parse
21
+ return unless value
22
+ case type
23
+ when :boolean
24
+ AttributeParser::Boolean.new(value).parse
25
+ when :date
26
+ AttributeParser::Date.new(value).parse
27
+ when :date_time
28
+ AttributeParser::DateTime.new(value).parse
29
+ when :decimal
30
+ AttributeParser::Decimal.new(value).parse
31
+ when :integer
32
+ AttributeParser::Integer.new(value).parse
33
+ when :object
34
+ AttributeParser::Object.new(value).parse
35
+ when :string
36
+ AttributeParser::String.new(value).parse
37
+ else
38
+ raise ArgumentError, "Invalid attribute type: #{type}"
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :value, :type
45
+ end
46
+
47
+ private_constant :AttributeParser
48
+ end
@@ -0,0 +1,21 @@
1
+ module RealPage
2
+ class AttributeParser
3
+ # Base class for attribute parsers.
4
+ class Base
5
+ # @param value [String] the response value from RealPage
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+
10
+ # @return [Object] the parsed attribute value
11
+ def parse
12
+ raise NotImplementedError,
13
+ "#{self.class.name} must implement #{__method__}"
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :value
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of a boolean attribute
6
+ class Boolean < Base
7
+ # Values that RealPage responds with for true
8
+ TRUE_VALUES = %w[1 true].freeze
9
+ private_constant :TRUE_VALUES
10
+
11
+ # Values that RealPage responds with for true
12
+ FALSE_VALUES = %w[0 false].freeze
13
+ private_constant :FALSE_VALUES
14
+
15
+ # @return [true|false] the parsed attribute value
16
+ # @raise [RealPage::Error::InvalidResponse] if the value doesn't parse
17
+ # into true or false
18
+ def parse
19
+ return true if TRUE_VALUES.include?(value)
20
+ return false if FALSE_VALUES.include?(value)
21
+ raise Error::InvalidResponse, "Invalid boolean response value: #{value}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of a date attribute
6
+ class Date < Base
7
+ # RealPage responds with multiple date formats. This is one format that
8
+ # Date.parse will not parse correctly, so we need special handling
9
+ FORMAT = '%m/%d/%Y'.freeze
10
+ private_constant :FORMAT
11
+
12
+ # @return [Date] the parsed attribute value
13
+ def parse
14
+ return if value == ''
15
+ if value =~ %r[/]
16
+ ::Date.strptime(value, FORMAT)
17
+ else
18
+ # RealPage sometimes returns 0001-01-01 for dates, which appears to be
19
+ # their representation of a NULL value.
20
+ date = ::Date.parse(value)
21
+ date.year == 1 ? nil : date
22
+ end
23
+ rescue ArgumentError
24
+ raise Error::InvalidResponse, "Invalid date response value: #{value}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ require 'tzinfo'
2
+
3
+ require_relative 'base'
4
+
5
+ module RealPage
6
+ class AttributeParser
7
+ # Parse the response value of a date with time attribute.
8
+ class DateTime < Base
9
+ # RealPage time strings are assumed to be in Central time.
10
+ TIME_ZONE = TZInfo::Timezone.get('America/Chicago')
11
+ private_constant :TIME_ZONE
12
+
13
+ # @return [Date] the parsed attribute value
14
+ def parse
15
+ return if value == ''
16
+ date_time = ::DateTime.parse(value)
17
+ TIME_ZONE.local_to_utc(date_time) { |periods| periods.last }
18
+ rescue ArgumentError
19
+ raise Error::InvalidResponse, "Invalid date/time response value: #{value}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of a decimal attribute
6
+ class Decimal < Base
7
+ # @return [Float] the parsed attribute value
8
+ def parse
9
+ Float(value)
10
+ rescue ArgumentError
11
+ raise Error::InvalidResponse, "Invalid decimal response value: #{value}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of an integer attribute
6
+ class Integer < Base
7
+ # @return [Integer] the parsed attribute value
8
+ def parse
9
+ Integer(value, 10)
10
+ rescue ArgumentError
11
+ raise Error::InvalidResponse, "Invalid integer response value: #{value}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of an object attribute
6
+ class Object < Base
7
+ # @return [Object] the parsed attribute value
8
+ def parse
9
+ value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'base'
2
+
3
+ module RealPage
4
+ class AttributeParser
5
+ # Parse the response value of a string attribute
6
+ class String < Base
7
+ # @return [String] the parsed attribute value
8
+ def parse
9
+ value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module RealPage
2
+ module DocumentParser
3
+ end
4
+
5
+ private_constant :DocumentParser
6
+ end
@@ -0,0 +1,51 @@
1
+ require 'multi_xml'
2
+
3
+ require 'real_page/validator/request_fault'
4
+ require 'real_page/document_parser'
5
+
6
+ module RealPage
7
+ module DocumentParser
8
+ # Base class for parsing RealPage responses. Subclasses must implement
9
+ # #parse_body and can optionally override #validator_classes to add more
10
+ # validation than just the default
11
+ class Base
12
+ DEFAULT_VALIDATOR_CLASSES = [Validator::RequestFault].freeze
13
+ private_constant :DEFAULT_VALIDATOR_CLASSES
14
+
15
+ # @param xml [String] the XML response from the request
16
+ # @option param request_params [Object] the request parameter
17
+ # @return [Object] the parsed object(s) from the rsesponse
18
+ # @raise [RealPage::Error::Base] if the response is invalid
19
+
20
+ def initialize(request_params: nil, request_name: nil)
21
+ @request_params = request_params
22
+ @request_name = request_name
23
+ end
24
+
25
+ def parse(xml)
26
+ parsed = MultiXml.parse(xml)
27
+ [*DEFAULT_VALIDATOR_CLASSES, *validator_classes].each do |klass|
28
+ klass.new(parsed, request_params, request_name).validate!
29
+ end
30
+ parse_body(parsed['s:Envelope']['s:Body'])
31
+ end
32
+
33
+ attr_reader :request_params, :request_name
34
+
35
+ private
36
+
37
+ # @param body [Hash<String, Object>] the body of the XML response parsed
38
+ # into a Hash
39
+ # @return [Object] the parsed object(s) from the rsesponse
40
+ # @raise [RealPage::Error::Base] if the response is invalid
41
+ def parse_body(body)
42
+ raise NotImplementedError,
43
+ "#{self.class.name} must implement #{__method__}"
44
+ end
45
+
46
+ def validator_classes
47
+ []
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ require 'real_page/utils/array_fetcher'
2
+
3
+ require_relative 'base'
4
+
5
+ module RealPage
6
+ module DocumentParser
7
+ # Parse the GetFloorPlanList response
8
+ class FloorPlanObject < Base
9
+ private
10
+
11
+ # @param body [Hash<String, Object>] the body of the XML response parsed
12
+ # into a Hash
13
+ # @return [Array<RealPage::Model::FloorPlan>] the floor_plans contained
14
+ # in the response
15
+ # @raise [RealPage::Error::Base] if the response is invalid
16
+ def parse_body(body)
17
+ Utils::ArrayFetcher.new(
18
+ hash: floor_plans(body),
19
+ key: 'FloorPlanObject',
20
+ model: Model::FloorPlan
21
+ ).fetch
22
+ end
23
+
24
+ def floor_plans(body)
25
+ response = body['getfloorplanlistResponse']
26
+ response['getfloorplanlistResult']['GetFloorPlanList']
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ require 'real_page/utils/array_fetcher'
2
+ require 'real_page/validator/prospects_data'
3
+
4
+ require_relative 'base'
5
+
6
+ module RealPage
7
+ module DocumentParser
8
+ # Parse the ProspectSearch response
9
+ class GuestCards < Base
10
+ MULTIPLE_CHILDREN = {
11
+ 'Activities' => { model: Model::Activity, key: 'Activity' },
12
+ 'FollowUps' => { model: Model::FollowUp, key: 'FollowUp' },
13
+ 'LeaseActions' => { model: Model::LeaseAction, key: 'LeaseAction' },
14
+ 'Quotes' => { model: Model::Quote, key: 'Quote' },
15
+ 'Screenings' => { model: Model::Screening, key: 'Screening' },
16
+ 'UnitsShown' => { model: Model::UnitShown, key: 'UnitShown' }
17
+ }
18
+ SINGLE_CHILDREN = %w[Appointment Preferences]
19
+ private_constant :MULTIPLE_CHILDREN, :SINGLE_CHILDREN
20
+
21
+ private
22
+
23
+ # @param body [Hash<String, Object>] the body of the XML response parsed
24
+ # into a Hash
25
+ # @return [Array<RealPage::Model::GuestCard>] the guest_cards contained
26
+ # in the response
27
+ # @raise [RealPage::Error::Base] if the response is invalid
28
+ def parse_body(body)
29
+ guest_cards(body).map do |guest_card|
30
+ attrs = guest_card.merge(children(guest_card))
31
+ attrs.delete('Amentities') # Remove the misspelled version
32
+ Model::GuestCard.new(attrs)
33
+ end
34
+ end
35
+
36
+ # @return [Hash<String,RealPage::Model] the guest_card's children that
37
+ # require more specialized parsing
38
+ def custom_children(guest_card)
39
+ {}.tap do |kids|
40
+ if guest_card['Amentities']
41
+ kids['Amenities'] = Amenities.new.parse(guest_card['Amentities'])
42
+ end
43
+ if guest_card['Prospects']
44
+ kids['Prospects'] = Prospects.new.parse(guest_card['Prospects'])
45
+ end
46
+ end
47
+ end
48
+
49
+ # @return [Hash] the guest_card's children, parsed into RealPage::Models
50
+ def children(guest_card)
51
+ single_children(guest_card)
52
+ .merge(multiple_children(guest_card))
53
+ .merge(custom_children(guest_card))
54
+ end
55
+
56
+ # @return [Array<Hash<String, Object>>] an array of all GuestCard
57
+ # attributes
58
+ def guest_cards(body)
59
+ response = body['prospectsearchResponse']
60
+ result = response['prospectsearchResult']
61
+ prospect_search = result['ProspectSearch'] if result
62
+ return [] unless prospect_search
63
+ guest_cards = prospect_search['GuestCards']
64
+ Utils::ArrayFetcher.new(hash: guest_cards, key: 'GuestCard').fetch
65
+ end
66
+
67
+ # @return [Hash<String, Array<RealPage::Model>] the guest_card's children
68
+ # that can have multiple entries
69
+ def multiple_children(guest_card)
70
+ Hash[
71
+ MULTIPLE_CHILDREN.map do |key, definition|
72
+ fetcher =
73
+ Utils::ArrayFetcher.new(definition.merge(hash: guest_card[key]))
74
+ [key, fetcher.fetch]
75
+ end
76
+ ]
77
+ end
78
+
79
+ # @return [Hash<String,RealPage::Model] the guest_card's children that
80
+ # have single entries
81
+ def single_children(guest_card)
82
+ Hash[
83
+ SINGLE_CHILDREN.map do |kid|
84
+ [kid, Model.const_get(kid).new(guest_card[kid])] if guest_card[kid]
85
+ end.compact
86
+ ]
87
+ end
88
+
89
+ def validator_classes
90
+ [Validator::ProspectsData]
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # This file has a circular dependency with its sub-parsers. They file uses
97
+ # the others to help parse the inner objects of the XML, and that file's class
98
+ # definition is nested within this class.
99
+ #
100
+ # Having the require at the bottom of the file keeps the class loader happy
101
+ require_relative 'guest_cards/prospects'
102
+ require_relative 'guest_cards/amenities'