amsi 1.0.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 (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