amsi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.ci +125 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.txt +20 -0
  6. data/CODEOWNERS +1 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +319 -0
  10. data/Rakefile +6 -0
  11. data/amsi.gemspec +32 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/config/multi_xml.rb +4 -0
  15. data/lib/amsi/attribute_parser/base.rb +23 -0
  16. data/lib/amsi/attribute_parser/boolean.rb +25 -0
  17. data/lib/amsi/attribute_parser/date.rb +28 -0
  18. data/lib/amsi/attribute_parser/date_time.rb +24 -0
  19. data/lib/amsi/attribute_parser/decimal.rb +15 -0
  20. data/lib/amsi/attribute_parser/integer.rb +15 -0
  21. data/lib/amsi/attribute_parser/string.rb +13 -0
  22. data/lib/amsi/attribute_parser.rb +45 -0
  23. data/lib/amsi/document_parser/base.rb +52 -0
  24. data/lib/amsi/document_parser/get_moveins.rb +66 -0
  25. data/lib/amsi/document_parser/guest_card_result.rb +31 -0
  26. data/lib/amsi/document_parser/leases.rb +46 -0
  27. data/lib/amsi/document_parser/properties.rb +103 -0
  28. data/lib/amsi/error/bad_request.rb +18 -0
  29. data/lib/amsi/error/base.rb +9 -0
  30. data/lib/amsi/error/invalid_response.rb +9 -0
  31. data/lib/amsi/error/request_fault.rb +18 -0
  32. data/lib/amsi/error/request_timeout.rb +9 -0
  33. data/lib/amsi/error/unparsable_response.rb +11 -0
  34. data/lib/amsi/model/address.rb +18 -0
  35. data/lib/amsi/model/base/attribute.rb +63 -0
  36. data/lib/amsi/model/base/attribute_store.rb +37 -0
  37. data/lib/amsi/model/base.rb +64 -0
  38. data/lib/amsi/model/guest_card.rb +18 -0
  39. data/lib/amsi/model/guest_card_result.rb +26 -0
  40. data/lib/amsi/model/lease.rb +63 -0
  41. data/lib/amsi/model/leasing_agent.rb +21 -0
  42. data/lib/amsi/model/manager.rb +16 -0
  43. data/lib/amsi/model/marketing_source.rb +21 -0
  44. data/lib/amsi/model/occupant.rb +32 -0
  45. data/lib/amsi/model/property.rb +35 -0
  46. data/lib/amsi/model/unit_type.rb +38 -0
  47. data/lib/amsi/parameter/contact_type.rb +10 -0
  48. data/lib/amsi/parameter/prospect.rb +19 -0
  49. data/lib/amsi/request/add_guest_card.rb +93 -0
  50. data/lib/amsi/request/base.rb +106 -0
  51. data/lib/amsi/request/get_moveins_by_first_marketing_source.rb +57 -0
  52. data/lib/amsi/request/get_property_list.rb +49 -0
  53. data/lib/amsi/request/get_property_residents.rb +45 -0
  54. data/lib/amsi/request_section/add_guest_card.rb +105 -0
  55. data/lib/amsi/request_section/auth.rb +22 -0
  56. data/lib/amsi/request_section/moveins_filter.rb +42 -0
  57. data/lib/amsi/request_section/property_list_filter.rb +45 -0
  58. data/lib/amsi/request_section/property_resident_filter.rb +32 -0
  59. data/lib/amsi/utils/request_fetcher.rb +37 -0
  60. data/lib/amsi/utils/request_generator.rb +54 -0
  61. data/lib/amsi/utils/snowflake_event_tracker.rb +113 -0
  62. data/lib/amsi/validator/base.rb +95 -0
  63. data/lib/amsi/validator/prospect_event_validator.rb +20 -0
  64. data/lib/amsi/validator/request_errors.rb +61 -0
  65. data/lib/amsi/validator/request_fault.rb +57 -0
  66. data/lib/amsi/validator/resident_event_validator.rb +20 -0
  67. data/lib/amsi/validator.rb +7 -0
  68. data/lib/amsi/version.rb +3 -0
  69. data/lib/amsi.rb +31 -0
  70. metadata +265 -0
@@ -0,0 +1,105 @@
1
+ module Amsi
2
+ module RequestSection
3
+ # Generate the AddGuestCard elements of an Amsi request
4
+ class AddGuestCard
5
+ # Formats dates and times like "mm/dd/yyyy", "02/14/2015"
6
+ DATE_FORMAT = '%m/%d/%Y'
7
+ private_constant :DATE_FORMAT
8
+
9
+ def initialize(
10
+ appointment_date: nil,
11
+ comments: nil,
12
+ contact_datetime: nil,
13
+ contact_type:,
14
+ date_needed: nil,
15
+ initial_source_id: nil,
16
+ lease_term_desired: nil,
17
+ leasing_agent_id: nil,
18
+ property_id:,
19
+ prospect:,
20
+ requirements: nil,
21
+ qualified: nil,
22
+ unit_subtype: nil,
23
+ unit_type: nil
24
+ )
25
+ @appointment_date = appointment_date
26
+ @comments = comments
27
+ @contact_datetime = contact_datetime
28
+ @contact_type = contact_type
29
+ @date_needed = date_needed
30
+ @initial_source_id = initial_source_id
31
+ @lease_term_desired = lease_term_desired
32
+ @leasing_agent_id = leasing_agent_id
33
+ @property_id = property_id
34
+ @prospect = prospect
35
+ @requirements = requirements
36
+ @qualified = qualified
37
+ @unit_subtype = unit_subtype
38
+ @unit_type = unit_type
39
+ end
40
+
41
+ def generate(xml_builder)
42
+ xml_builder.XMLData do |xml_data|
43
+ xml_data.cdata edex.to_s
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :appointment_date,
50
+ :comments,
51
+ :contact_datetime,
52
+ :contact_type,
53
+ :date_needed,
54
+ :initial_source_id,
55
+ :lease_term_desired,
56
+ :leasing_agent_id,
57
+ :property_id,
58
+ :prospect,
59
+ :requirements,
60
+ :qualified,
61
+ :unit_subtype,
62
+ :unit_type
63
+
64
+ def edex
65
+ Nokogiri::XML::Builder.new do |xml_builder|
66
+ xml_builder.EDEX do |edex|
67
+ edex.propertyid(property_id)
68
+ edex.contacttype(contact_type)
69
+ edex.contactdatetime(contact_time) if contact_datetime
70
+ edex.modifieddate(format_date(Date.today))
71
+ edex.initialsourceid(initial_source_id) if initial_source_id
72
+ edex.qualified(qualified ? 'Y' : 'N') unless qualified.nil?
73
+ edex.leasetermdesired(lease_term_desired) if lease_term_desired
74
+ edex.firstname(prospect.first_name) if prospect.first_name
75
+ edex.lastname(prospect.last_name)
76
+ edex.dateneeded(format_date(date_needed)) if date_needed
77
+ edex.daytimephone(prospect.daytime_phone) if prospect.daytime_phone
78
+ edex.email(prospect.email) if prospect.email
79
+ edex.appointmentdate(appointment) if appointment_date
80
+ edex.unittype(unit_type) if unit_type
81
+ edex.unitsubtype(unit_subtype) if unit_subtype
82
+ edex.leasingagentid(leasing_agent_id) if leasing_agent_id
83
+ edex.requirements(requirements) if requirements
84
+ edex.comments do |comments_xml|
85
+ comments_xml.comment_(comments, date: format_date(Date.today))
86
+ end if comments
87
+ end
88
+ end.doc.root
89
+ end
90
+
91
+ def contact_time
92
+ contact_datetime.strftime('%m/%d/%Y %0l:%M %p')
93
+ end
94
+
95
+ def appointment
96
+ format_date(appointment_date)
97
+ end
98
+
99
+ def format_date(date)
100
+ # Formats dates and times like "mm/dd/yyyy", "02/14/2015"
101
+ date.strftime(DATE_FORMAT)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,22 @@
1
+ module Amsi
2
+ module RequestSection
3
+ # Generate the auth elements of an AMSI request
4
+ class Auth
5
+ def initialize(user_id:, password:, portfolio_name:)
6
+ @user_id = user_id
7
+ @password = password
8
+ @portfolio_name = portfolio_name
9
+ end
10
+
11
+ def generate(xml_builder)
12
+ xml_builder.UserID user_id
13
+ xml_builder.Password password
14
+ xml_builder.PortfolioName portfolio_name
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :user_id, :password, :portfolio_name
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ module Amsi
2
+ module RequestSection
3
+ # Generate the move in filter elements of an Amsi request
4
+ class MoveinsFilter
5
+ def initialize(
6
+ property_id:,
7
+ marketing_source_code:,
8
+ from_date:,
9
+ through_date:
10
+ )
11
+ @property_id = property_id
12
+ @marketing_source_code = marketing_source_code
13
+ @from_date = from_date
14
+ @through_date = through_date
15
+ end
16
+
17
+ def generate(xml_builder)
18
+ xml_builder.XMLData do |xml_data|
19
+ xml_data.cdata edex.to_s
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def edex
26
+ Nokogiri::XML::Builder.new do |xml_builder|
27
+ xml_builder.EDEX do |edex|
28
+ edex.propertyid(property_id)
29
+ edex.marketingsourcecode(marketing_source_code)
30
+ edex.fromdate(from_date.to_s)
31
+ edex.thrudate(through_date.to_s)
32
+ end
33
+ end.doc.root
34
+ end
35
+
36
+ attr_reader :from_date,
37
+ :marketing_source_code,
38
+ :property_id,
39
+ :through_date
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ module Amsi
2
+ module RequestSection
3
+ # Generate the property list filter elements of an AMSI request
4
+ class PropertyListFilter
5
+ def initialize(
6
+ include_leasing_agents: false,
7
+ include_marketing_sources: false,
8
+ include_unit_types: false,
9
+ property_id: nil
10
+ )
11
+ @include_leasing_agents = include_leasing_agents
12
+ @include_marketing_sources = include_marketing_sources
13
+ @include_unit_types = include_unit_types
14
+ @property_id = property_id
15
+ end
16
+
17
+ def generate(xml_builder)
18
+ xml_builder.XMLData do |xml_data|
19
+ xml_data.cdata edex.to_s
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def edex
26
+ Nokogiri::XML::Builder.new do |xml_builder|
27
+ xml_builder.EDEX do |edex|
28
+ edex.propertyid(property_id)
29
+ edex.includeamenities('0')
30
+ edex.includeclasses('0')
31
+ edex.includeincomecodes('0')
32
+ edex.includeleasingagents(include_leasing_agents ? '1' : '0')
33
+ edex.includemarketingsources(include_marketing_sources ? '1' : '0')
34
+ edex.includeunittypes(include_unit_types ? '1' : '0')
35
+ end
36
+ end.doc.root
37
+ end
38
+
39
+ attr_reader :include_leasing_agents,
40
+ :include_marketing_sources,
41
+ :include_unit_types,
42
+ :property_id
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module Amsi
2
+ module RequestSection
3
+ # Generate the property resident filter elements of an AMSI request
4
+ class PropertyResidentFilter
5
+ def initialize(property_id:, lease_status:, include_marketing_source: false)
6
+ @property_id = property_id
7
+ @lease_status = lease_status
8
+ @include_marketing_source = include_marketing_source
9
+ end
10
+
11
+ def generate(xml_builder)
12
+ xml_builder.XMLData do |xml_data|
13
+ xml_data.cdata edex.to_s
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def edex
20
+ Nokogiri::XML::Builder.new do |xml_builder|
21
+ xml_builder.EDEX do |edex|
22
+ edex.propertyid property_id
23
+ edex.leasestatus lease_status
24
+ edex.includemarketingsource include_marketing_source ? '1' : '0'
25
+ end
26
+ end.doc.root
27
+ end
28
+
29
+ attr_reader :property_id, :lease_status, :include_marketing_source
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ require 'faraday'
2
+
3
+ module Amsi
4
+ module Utils
5
+ # Send a SOAP request to AMSI
6
+ class RequestFetcher
7
+ module Status
8
+ TIMEOUT = 504
9
+ end
10
+
11
+ # @param generator [RequestGenerator] an instance of a RequestGenerator
12
+ def initialize(generator:, conn: Faraday.new)
13
+ @generator = generator
14
+ @conn = conn
15
+ end
16
+
17
+ # @return [String] the XML response from AMSI
18
+ def fetch
19
+ response = conn.post(generator.url) do |request|
20
+ request.body = generator.body
21
+ request.headers = generator.headers
22
+ end
23
+
24
+ if response.status == Status::TIMEOUT
25
+ raise Error::RequestTimeout,
26
+ 'The server did not respond in a reasonable timeframe'
27
+ end
28
+
29
+ response.body
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :generator, :conn
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ require 'nokogiri'
2
+
3
+ module Amsi
4
+ module Utils
5
+ # Generate a SOAP request for a specific action and sections
6
+ class RequestGenerator
7
+ ENVELOPE_ATTRIBUTES = {
8
+ 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/'
9
+ }.freeze
10
+
11
+ ACTION_ATTRIBUTES = {
12
+ 'xmlns' => 'http://tempuri.org/'
13
+ }.freeze
14
+
15
+ # @param url: [String] the URL to POST to
16
+ # @param soap_action: [String] the PascalCase action to request from AMSI.
17
+ # @param sections: [Array<RequestSection>] the setion generators that will
18
+ # be used to generate the body of the XML request
19
+ def initialize(url:, soap_action:, sections:)
20
+ @url = url
21
+ @soap_action = soap_action
22
+ @sections = sections
23
+ end
24
+
25
+ def headers
26
+ {
27
+ 'Content-Type' => 'text/xml; charset=utf-8',
28
+ 'SOAPAction' => "http://tempuri.org/#{soap_action}"
29
+ }
30
+ end
31
+
32
+ # @return [String] the XML request for the specified action and sections
33
+ def body
34
+ Nokogiri::XML::Builder.new do |xml_builder|
35
+ xml_builder['soap'].Envelope(ENVELOPE_ATTRIBUTES) do
36
+ xml_builder['soap'].Body do
37
+ xml_builder.send(soap_action, ACTION_ATTRIBUTES) do
38
+ sections.each do |section|
39
+ section.generate(xml_builder)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end.to_xml
45
+ end
46
+
47
+ attr_reader :url
48
+
49
+ private
50
+
51
+ attr_reader :soap_action, :sections
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,113 @@
1
+ require 'event_tracker'
2
+
3
+ module Amsi
4
+ module Utils
5
+ class SnowflakeEventTracker
6
+ IMPORT_PMS_RESIDENT_EVENT = 'import_pms_resident'
7
+ IMPORT_PMS_PROSPECT_EVENT = 'import_pms_prospect'
8
+
9
+ def self.track_pms_resident_event(
10
+ remote_lease_id: nil,
11
+ import_resident_id:,
12
+ resident_type:,
13
+ request_params:,
14
+ move_in_date: nil,
15
+ lease_to: nil,
16
+ lease_from: nil,
17
+ first_name_present: false,
18
+ last_name_present: false,
19
+ email_present: false,
20
+ phones_count: 0,
21
+ unit_name:,
22
+ resident_id: nil,
23
+ error: nil
24
+ )
25
+ EventTracker.track_process_events(name: IMPORT_PMS_RESIDENT_EVENT) do |events|
26
+ events.add_imported_event(
27
+ EventTracker::ResourceFactory.build_pms_resident(
28
+ billing_import: EventTracker::BillingImportFactory.build_billing_import(
29
+ property_id: request_params[:property_id],
30
+ billing_config_id: request_params[:billing_config_id],
31
+ remote_id: request_params[:remote_id],
32
+ pms_type: 'amsi',
33
+ import_id: request_params[:import_id],
34
+ pmc_id: request_params[:pmc_id],
35
+ service: 'amsi'
36
+ ),
37
+ remote_lease_id: remote_lease_id,
38
+ import_resident_id: import_resident_id,
39
+ resident_type: resident_type,
40
+ api_name: 'GetPropertyResidents',
41
+ request_params: EventTracker::ResourceFactory::PmsResident.build_request_params(
42
+ start_date: request_params[:start_date].nil? ? nil : request_params[:start_date].to_time,
43
+ end_date: request_params[:end_date].nil? ? nil : request_params[:end_date].to_time,
44
+ prospect_id: nil,
45
+ pmc_id: request_params[:pmc_id],
46
+ remote_id: request_params[:remote_id],
47
+ traffic_source_id: nil
48
+ ),
49
+ move_in_date: move_in_date,
50
+ lease_to: lease_to,
51
+ lease_from: lease_from,
52
+ first_name_present: first_name_present,
53
+ last_name_present: last_name_present,
54
+ email_present: email_present,
55
+ phones_count: phones_count,
56
+ unit_name: unit_name,
57
+ resident_id: resident_id,
58
+ move_in_report_type: 'amsi_api',
59
+ error: error
60
+ )
61
+ )
62
+ end
63
+ end
64
+
65
+ def self.track_pms_prospect_event(
66
+ remote_lease_id: nil,
67
+ request_params:,
68
+ first_name_present:,
69
+ last_name_present:,
70
+ email_present:,
71
+ phone_present:,
72
+ contact_date: nil,
73
+ contact_source: nil,
74
+ remote_prospect_id: nil,
75
+ error: nil
76
+ )
77
+ EventTracker.track_process_events(name: IMPORT_PMS_PROSPECT_EVENT) do |events|
78
+ events.add_imported_event(
79
+ EventTracker::ResourceFactory.build_pms_prospect(
80
+ billing_import: EventTracker::BillingImportFactory.build_billing_import(
81
+ property_id: request_params[:property_id],
82
+ billing_config_id: request_params[:billing_config_id],
83
+ remote_id: request_params[:remote_id],
84
+ pms_type: 'amsi',
85
+ import_id: request_params[:import_id],
86
+ pmc_id: request_params[:pmc_id],
87
+ service: 'amsi'
88
+ ),
89
+ remote_lease_id: remote_lease_id,
90
+ import_resident_id: request_params[:import_resident_id] || '',
91
+ resident_type: 'PRIMARY',
92
+ api_name: 'GetPropertyResidents',
93
+ request_params: EventTracker::ResourceFactory::PmsProspect.build_request_params(
94
+ pmc_id: request_params[:pmc_id],
95
+ remote_id: request_params[:remote_id],
96
+ prospect_id: request_params[:prospect_id],
97
+ first_name_present: first_name_present,
98
+ last_name_present: last_name_present,
99
+ email_present: email_present,
100
+ phone_present: phone_present,
101
+ ),
102
+ contact_date: contact_date,
103
+ contact_source: contact_source,
104
+ remote_prospect_id: remote_prospect_id,
105
+ move_in_report_type: 'amsi_api',
106
+ error: error
107
+ )
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,95 @@
1
+ require 'amsi/validator'
2
+ require 'amsi/utils/snowflake_event_tracker'
3
+ require 'securerandom'
4
+
5
+ module Amsi
6
+ module Validator
7
+ class Base
8
+ def send_pms_resident_event(lease:, params:, error_message: nil)
9
+ remote_lease_id, lease_to, lease_from = [ nil ] * 3
10
+ if lease
11
+ unit_name = lease.unit_id
12
+ move_in_date = lease.move_in_date
13
+ else
14
+ unit_name = ''
15
+ move_in_date = nil
16
+ end
17
+ lease.occupants.each_with_index do |occupant, index|
18
+ Utils::SnowflakeEventTracker.track_pms_resident_event(
19
+ remote_lease_id: remote_lease_id,
20
+ import_resident_id: import_resident_id(params),
21
+ resident_type: index == 0 ? 'PRIMARY': 'ROOMMATE',
22
+ request_params: pms_resident_request_params(params),
23
+ move_in_date: move_in_date&.to_time,
24
+ lease_to: lease_to,
25
+ lease_from: lease_from,
26
+ first_name_present: is_present?(occupant.first_name),
27
+ last_name_present: is_present?(occupant.last_name),
28
+ email_present: is_present?(occupant.email),
29
+ phones_count: [occupant.phone_1, occupant.phone_2].compact.count,
30
+ unit_name: unit_name,
31
+ resident_id: occupant.resident_id,
32
+ error: error_message
33
+ )
34
+ end
35
+ end
36
+
37
+ def pms_resident_request_params(params)
38
+ {
39
+ start_date: params[:start_date]&.to_time,
40
+ end_date: params[:end_date]&.to_time,
41
+ prospect_id: nil,
42
+ pmc_id: params[:pmc_id],
43
+ remote_id: params[:remote_id],
44
+ traffic_source_id: nil,
45
+ import_id: params[:import_id],
46
+ billing_config_id: params[:billing_config]&.id,
47
+ property_id: params[:billing_config]&.property_id
48
+ }
49
+ end
50
+
51
+ def send_pms_prospect_event(lease:, params:, error_message: nil)
52
+ lease.occupants.each do |occupant|
53
+ Utils::SnowflakeEventTracker.track_pms_prospect_event(
54
+ remote_lease_id: nil,
55
+ request_params: pms_prospect_request_params(params),
56
+ first_name_present: is_present?(occupant.first_name),
57
+ last_name_present: is_present?(occupant.last_name),
58
+ email_present: is_present?(occupant.email),
59
+ phone_present: is_present?(occupant.phone_1) || is_present?(occupant.phone_2),
60
+ contact_date: nil,
61
+ contact_source: lease.lead_source_code,
62
+ remote_prospect_id: nil,
63
+ error: error_message
64
+ )
65
+ end
66
+ end
67
+
68
+ def pms_prospect_request_params(params)
69
+ {
70
+ billing_config_id: params[:billing_config]&.id,
71
+ property_id: params[:billing_config]&.property_id,
72
+ remote_id: params[:remote_id],
73
+ import_id: params[:import_id],
74
+ pmc_id: params[:pmc_id],
75
+ import_resident_id: import_resident_id(params),
76
+ prospect_id: nil
77
+ }
78
+ end
79
+
80
+ def import_resident_id(params)
81
+ "#{params[:import_id] || ''}-#{SecureRandom.alphanumeric(15)}"
82
+ end
83
+
84
+ def is_present?(value)
85
+ if value.is_a?(String)
86
+ !value.strip.empty?
87
+ elsif value.is_a?(Hash) || value.is_a?(Array)
88
+ !(value.empty? || value.nil?)
89
+ else
90
+ !value.nil?
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,20 @@
1
+ require 'amsi/validator'
2
+ require_relative 'base'
3
+
4
+ module Amsi
5
+ module Validator
6
+ class ProspectEventValidator < Base
7
+
8
+ def initialize(response = nil)
9
+ end
10
+
11
+ def validate!
12
+ end
13
+
14
+ # Send parased response's individual prospect data to Snowflake via event tracker
15
+ def send_event(lease, params)
16
+ send_pms_prospect_event(lease: lease, params: params || {})
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,61 @@
1
+ require 'amsi/error/bad_request'
2
+
3
+ module Amsi
4
+ module Validator
5
+ # Ensure there are no errors in the wrapped contents of the body.
6
+ #
7
+ # This validator works on responses that have the following format:
8
+ #
9
+ # <?xml version="1.0" encoding="utf-8"?>
10
+ # <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
11
+ # <soap:Body>
12
+ # <GetPropertyResidentsResponse xmlns="http://tempuri.org/">
13
+ # <GetPropertyResidentsResult>&lt;InternetTrafficResponse&gt;&lt;webservice&gt;Leasing&lt;/webservice&gt;&lt;webmethod&gt;GetPropertyResidents&lt;/webmethod&gt;&lt;Error&gt;&lt;ErrorCode&gt;1&lt;/ErrorCode&gt;&lt;ErrorDescription&gt;Validation of user credentials failed.&lt;/ErrorDescription&gt;&lt;/Error&gt;&lt;/InternetTrafficResponse&gt;</GetPropertyResidentsResult>
14
+ # </GetPropertyResidentsResponse>
15
+ # </soap:Body>
16
+ # </soap:Envelope>
17
+
18
+ class RequestErrors
19
+ # @param response [Hash<String, Object>] the XML response parsed into a
20
+ # Hash
21
+ def initialize(response)
22
+ @response = response
23
+ end
24
+
25
+ # @raise [Amsi::Error::BadRequest] if the response has an error
26
+ # node in the contents
27
+ def validate!
28
+ return unless error?
29
+ raise Amsi::Error::BadRequest.new([error])
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :response
35
+
36
+ def error_contents
37
+ body = response['soap:Envelope']['soap:Body']
38
+ response_key = body.keys.detect { |key| key !~ /^xmlns/ }
39
+ contents_response = body[response_key]
40
+ result_key = contents_response.keys.grep(/Result$/).first
41
+ escaped_result = contents_response[result_key]
42
+ parsed_result = MultiXml.parse(escaped_result)
43
+
44
+ parsed_result['InternetTrafficResponse']
45
+ end
46
+
47
+ def error?
48
+ error_contents
49
+ end
50
+
51
+ def error
52
+ error = error_contents['Error']
53
+
54
+ Struct.new(:message, :error_code).new(
55
+ error['ErrorDescription'],
56
+ error['ErrorCode']
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ module Amsi
2
+ module Validator
3
+ # Ensure there is no 'Fault' node in the response body
4
+ class RequestFault
5
+ FaultDetails = Struct.new(:custom_message, :exception_message, :log_id)
6
+ private_constant :FaultDetails
7
+
8
+ # @param response [Hash<String, Object>] the XML response parsed into a
9
+ # Hash
10
+ def initialize(response)
11
+ @response = response
12
+ end
13
+
14
+ # @raise [Amsi::Error::RequestFault] if the response has a fault
15
+ def validate!
16
+ return unless error?
17
+ raise Error::RequestFault.new(error_message, fault_code, details)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :response
23
+
24
+ def fault
25
+ response['soap:Envelope']['soap:Body']['soap:Fault']
26
+ end
27
+
28
+ def details
29
+ return unless fault['detail']
30
+ app_fault = fault['detail'].values.first
31
+ FaultDetails.new(
32
+ app_fault['CustomMessage'],
33
+ app_fault['ExceptionMessage'],
34
+ app_fault['LogId']
35
+ )
36
+ end
37
+
38
+ def error?
39
+ !fault.nil?
40
+ end
41
+
42
+ def fault_code
43
+ return unless fault
44
+ code = fault['faultcode']
45
+ code = code['__content__'] if code.is_a?(Hash)
46
+ code.strip
47
+ end
48
+
49
+ def error_message
50
+ return unless fault
51
+ fault_string = fault['faultstring']
52
+ fault_string = fault_string['__content__'] if fault_string.is_a?(Hash)
53
+ fault_string.strip
54
+ end
55
+ end
56
+ end
57
+ end