From 2c54e40b6d8ceea44dcfa23665b0538ee6d2b03c Mon Sep 17 00:00:00 2001 From: Rachal Cassity Date: Wed, 6 Nov 2024 15:05:54 -0600 Subject: [PATCH 1/2] Created AddressValidationService for AddressValidation API V3 (#18895) * Created AddressValidationService for AddressValidation API V3 * Corrected intPostalCode in validation address * fixed lint fail * Used dig for parameters --- .rubocop_todo.yml | 1 + .../profile/address_validation_controller.rb | 16 +- app/swagger/swagger/requests/profile.rb | 1 - config/initializers/breakers.rb | 2 + config/settings.yml | 1 + .../models/v3/validation_address.rb | 79 +++ .../address_suggestions_response.rb | 36 ++ .../v3/address_validation/configuration.rb | 23 + .../v3/address_validation/service.rb | 65 +++ .../v0/address_validation_controller.rb | 15 +- .../mobile/v0/profile_base_controller.rb | 1 + .../app/sidekiq/representatives/update.rb | 67 ++- .../lib/tasks/update_vso_addresses.rake | 17 +- .../sidekiq/representatives/update_spec.rb | 534 +++++++++++++++++- .../va_profile/v3/validation_address.rb | 28 + .../models/v3/validation_address_spec.rb | 119 ++++ .../v3/address_validation/service_spec.rb | 229 ++++++++ spec/requests/swagger_spec.rb | 4 +- spec/spec_helper.rb | 1 + spec/support/vcr.rb | 1 + .../candidate_multiple_matches.yml | 90 +++ .../address_validation/candidate_no_match.yml | 89 +++ 22 files changed, 1397 insertions(+), 22 deletions(-) create mode 100644 lib/va_profile/models/v3/validation_address.rb create mode 100644 lib/va_profile/v3/address_validation/address_suggestions_response.rb create mode 100644 lib/va_profile/v3/address_validation/configuration.rb create mode 100644 lib/va_profile/v3/address_validation/service.rb create mode 100644 spec/factories/va_profile/v3/validation_address.rb create mode 100644 spec/lib/va_profile/models/v3/validation_address_spec.rb create mode 100644 spec/lib/va_profile/v3/address_validation/service_spec.rb create mode 100644 spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_multiple_matches.yml create mode 100644 spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_no_match.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3421a4014c2..97ec5832b48 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -299,6 +299,7 @@ Lint/MissingSuper: - 'lib/search_typeahead/service.rb' - 'lib/token_validation/v2/client.rb' - 'lib/va_profile/address_validation/service.rb' + - 'lib/va_profile/v3/address_validation/service.rb' - 'lib/va_profile/service.rb' - 'lib/vbs/requests/list_statements.rb' - 'lib/vic/id_card_attribute_error.rb' diff --git a/app/controllers/v0/profile/address_validation_controller.rb b/app/controllers/v0/profile/address_validation_controller.rb index a815cbb064a..4c6634660a2 100644 --- a/app/controllers/v0/profile/address_validation_controller.rb +++ b/app/controllers/v0/profile/address_validation_controller.rb @@ -2,6 +2,8 @@ require 'va_profile/models/validation_address' require 'va_profile/address_validation/service' +require 'va_profile/models/v3/validation_address' +require 'va_profile/v3/address_validation/service' module V0 module Profile @@ -11,7 +13,12 @@ class AddressValidationController < ApplicationController skip_before_action :authenticate, only: [:create] def create - address = VAProfile::Models::ValidationAddress.new(address_params) + address = if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::Models::V3::ValidationAddress.new(address_params) + else + VAProfile::Models::ValidationAddress.new(address_params) + end + raise Common::Exceptions::ValidationErrors, address unless address.valid? Rails.logger.warn('AddressValidationController#create request completed', sso_logging_info) @@ -29,6 +36,7 @@ def address_params :address_pou, :address_type, :city, + :country_name, :country_code_iso3, :international_postal_code, :province, @@ -39,7 +47,11 @@ def address_params end def service - @service ||= VAProfile::AddressValidation::Service.new + @service ||= if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::V3::AddressValidation::Service.new + else + VAProfile::AddressValidation::Service.new + end end end end diff --git a/app/swagger/swagger/requests/profile.rb b/app/swagger/swagger/requests/profile.rb index 8beff5038bc..439b6d0c700 100644 --- a/app/swagger/swagger/requests/profile.rb +++ b/app/swagger/swagger/requests/profile.rb @@ -401,7 +401,6 @@ class Profile key :description, 'Response is OK' schema do key :type, :object - property(:validation_key, type: :integer) property(:addresses) do key :type, :array diff --git a/config/initializers/breakers.rb b/config/initializers/breakers.rb index 9bd77b61cbb..c9d4f1188f3 100644 --- a/config/initializers/breakers.rb +++ b/config/initializers/breakers.rb @@ -29,6 +29,7 @@ require 'search_typeahead/configuration' require 'search_click_tracking/configuration' require 'va_profile/address_validation/configuration' +require 'va_profile/v3/address_validation/configuration' require 'va_profile/contact_information/configuration' require 'va_profile/v2/contact_information/configuration' require 'va_profile/communication/configuration' @@ -64,6 +65,7 @@ Preneeds::Configuration.instance.breakers_service, SM::Configuration.instance.breakers_service, VAProfile::AddressValidation::Configuration.instance.breakers_service, + VAProfile::V3::AddressValidation::Configuration.instance.breakers_service, VAProfile::ContactInformation::Configuration.instance.breakers_service, VAProfile::V2::ContactInformation::Configuration.instance.breakers_service, VAProfile::Communication::Configuration.instance.breakers_service, diff --git a/config/settings.yml b/config/settings.yml index b4cebbf2326..48724795f2d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -280,6 +280,7 @@ vet360: timeout: 30 mock: false address_validation: + url: "https://sandbox-api.va.gov" hostname: "sandbox-api.va.gov" api_key: "" profile_information: diff --git a/lib/va_profile/models/v3/validation_address.rb b/lib/va_profile/models/v3/validation_address.rb new file mode 100644 index 00000000000..102e1bfc1fe --- /dev/null +++ b/lib/va_profile/models/v3/validation_address.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative 'base_address' +require 'common/hash_helpers' + +module VAProfile + module Models + module V3 + # Model for addresses sent and received from the VA profile address validation API + # AddressValidationV is used for ProfileServiceV3 and ContactInformationV2 + class ValidationAddress < V3::BaseAddress + # Convert a ValidationAddress into a hash that can be sent to the address validation + # API + # @return [Hash] hash that is formatted for POSTing to address validation API + def address_validation_req + Common::HashHelpers.deep_remove_blanks( + address: attributes.slice( + :address_line1, + :address_line2, + :address_line3 + ).deep_transform_keys { |k| k.to_s.camelize(:lower) }.merge( + intPostalCode: @international_postal_code, + cityName: @city, + zipCode5: @zip_code, + zipCode4: @zip_code_suffix, + country: { countryCodeISO2: @country_code_iso2, countryCodeISO3: @country_code_iso3, + countryName: @country_name, countryCodeFIPS: @country_code_fips }, + state: { stateCode: @state_code, stateName: @state_name }, + province: { provinceName: @province_name, provinceCode: @province_code }, + addressPOU: @address_pou + ) + ) + end + + # @return [VAProfile::Models::V3::ValidationAddress] validation address model created from + # address validation API response + def self.build_from_address_suggestion(address_suggestion_hash) + address_type = address_suggestion_hash['address_type'].upcase + attributes = { + address_line1: address_suggestion_hash['address_line1'], + address_line2: address_suggestion_hash['address_line2'], + address_line3: address_suggestion_hash['address_line3'], + address_type:, + city: address_suggestion_hash['city_name'], + country_name: address_suggestion_hash.dig('country', 'country_name'), + country_code_iso3: address_suggestion_hash.dig('country', 'country_code_iso3') + }.merge(regional_attributes(address_type, address_suggestion_hash)) + + new(attributes) + end + + def self.regional_attributes(address_type, address_hash) + if address_type == INTERNATIONAL + { + province: address_hash['province']['province_name'], + international_postal_code: address_hash['int_postal_code'] + } + else + { + state_code: address_hash.dig('state', 'state_code'), + county_code: address_hash.dig('county', 'county_code'), + county_name: address_hash.dig('county', 'county_name'), + zip_code: address_hash['zip_code5'], + zip_code_suffix: address_hash['zip_code4'] + } + end + end + + def self.build_address_metadata(address_suggestion_hash) + { + confidence_score: address_suggestion_hash['confidence'], + address_type: address_suggestion_hash['address_type'], + delivery_point_validation: address_suggestion_hash['delivery_point_validation'] + } + end + end + end + end +end diff --git a/lib/va_profile/v3/address_validation/address_suggestions_response.rb b/lib/va_profile/v3/address_validation/address_suggestions_response.rb new file mode 100644 index 00000000000..5dfa67f157e --- /dev/null +++ b/lib/va_profile/v3/address_validation/address_suggestions_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'va_profile/models/v3/validation_address' +require_relative 'service' + +module VAProfile + module V3 + module AddressValidation + # Wrapper for response from VA profile address validation API. + # Contains address suggestions and validation key used to ignore suggested addresses + # and use original address. + class AddressSuggestionsResponse + def initialize(candidate_res) + validation_key = candidate_res['override_validation_key'] + @response = { + addresses: candidate_res['candidate_addresses'].map do |address_suggestion_hash| + { + address: VAProfile::Models::V3::ValidationAddress.build_from_address_suggestion( + address_suggestion_hash + ).to_h.compact, + address_meta_data: VAProfile::Models::V3::ValidationAddress.build_address_metadata( + address_suggestion_hash + ).to_h + } + end, + validation_key: + } + end + + def to_json(*_args) + @response.to_json + end + end + end + end +end diff --git a/lib/va_profile/v3/address_validation/configuration.rb b/lib/va_profile/v3/address_validation/configuration.rb new file mode 100644 index 00000000000..97caa53674c --- /dev/null +++ b/lib/va_profile/v3/address_validation/configuration.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'va_profile/configuration' + +module VAProfile + module V3 + module AddressValidation + class Configuration < VAProfile::Configuration + def base_path + "#{VAProfile::Configuration::SETTINGS.address_validation.url}/services/address-validation/v3/" + end + + def base_request_headers + super.merge('apiKey' => VAProfile::Configuration::SETTINGS.address_validation.api_key) + end + + def service_name + 'VAProfile/V3/AddressValidation' + end + end + end + end +end diff --git a/lib/va_profile/v3/address_validation/service.rb b/lib/va_profile/v3/address_validation/service.rb new file mode 100644 index 00000000000..0aae17889e9 --- /dev/null +++ b/lib/va_profile/v3/address_validation/service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'common/client/concerns/monitoring' +require 'common/exceptions' +require_relative 'configuration' +require_relative 'address_suggestions_response' +require 'va_profile/service' +require 'va_profile/stats' + +module VAProfile + module V3 + module AddressValidation + # Wrapper for the VA profile address validation/suggestions API + class Service < VAProfile::Service + include Common::Client::Concerns::Monitoring + + STATSD_KEY_PREFIX = "#{VAProfile::Service::STATSD_KEY_PREFIX}.address_validation".freeze + configuration VAProfile::V3::AddressValidation::Configuration + + def initialize; end + + # Get address suggestions and override key from the VA profile API + # @return [VAProfile::AddressValidation::AddressSuggestionsResponse] response wrapper around address + # suggestions data + def address_suggestions(address) + with_monitoring do + candidate_res = candidate(address) + + AddressSuggestionsResponse.new(candidate_res) + end + end + + # @return [Hash] raw data from VA profile address validation API including + # address suggestions, validation key, and address errors + def candidate(address) + begin + res = perform( + :post, + 'candidate', + address.address_validation_req.to_json + ) + rescue => e + handle_error(e) + end + + res.body + end + + private + + def handle_error(error) + raise error unless error.is_a?(Common::Client::Errors::ClientError) + + save_error_details(error) + raise_invalid_body(error, self.class) unless error.body.is_a?(Hash) + + raise Common::Exceptions::BackendServiceException.new( + 'VET360_AV_ERROR', + detail: error.body + ) + end + end + end + end +end diff --git a/modules/ask_va_api/app/controllers/ask_va_api/v0/address_validation_controller.rb b/modules/ask_va_api/app/controllers/ask_va_api/v0/address_validation_controller.rb index 64f5a5112d9..084d261c2d3 100644 --- a/modules/ask_va_api/app/controllers/ask_va_api/v0/address_validation_controller.rb +++ b/modules/ask_va_api/app/controllers/ask_va_api/v0/address_validation_controller.rb @@ -2,6 +2,8 @@ require 'va_profile/models/validation_address' require 'va_profile/address_validation/service' +require 'va_profile/models/v3/validation_address' +require 'va_profile/v3/address_validation/service' module AskVAApi module V0 @@ -11,7 +13,12 @@ class AddressValidationController < ApplicationController service_tag 'profile' def create - address = VAProfile::Models::ValidationAddress.new(address_params) + validation_model = if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::Models::V3::ValidationAddress + else + VAProfile::Models::ValidationAddress + end + address = validation_model.new(address_params) raise Common::Exceptions::ValidationErrors, address unless address.valid? Rails.logger.warn('AddressValidationController#create request completed', sso_logging_info) @@ -39,7 +46,11 @@ def address_params end def service - @service ||= VAProfile::AddressValidation::Service.new + @service ||= if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::V3::AddressValidation::Service.new + else + VAProfile::AddressValidation::Service.new + end end end end diff --git a/modules/mobile/app/controllers/mobile/v0/profile_base_controller.rb b/modules/mobile/app/controllers/mobile/v0/profile_base_controller.rb index 07a2555670a..1181c5ec17e 100644 --- a/modules/mobile/app/controllers/mobile/v0/profile_base_controller.rb +++ b/modules/mobile/app/controllers/mobile/v0/profile_base_controller.rb @@ -2,6 +2,7 @@ require 'va_profile/address_validation/service' require_relative '../concerns/sso_logging' +require 'va_profile/v3/address_validation/service' module Mobile module V0 diff --git a/modules/veteran/app/sidekiq/representatives/update.rb b/modules/veteran/app/sidekiq/representatives/update.rb index d98aec7d30c..5e7f035947b 100644 --- a/modules/veteran/app/sidekiq/representatives/update.rb +++ b/modules/veteran/app/sidekiq/representatives/update.rb @@ -4,6 +4,8 @@ require 'sentry_logging' require 'va_profile/models/validation_address' require 'va_profile/address_validation/service' +require 'va_profile/models/v3/validation_address' +require 'va_profile/v3/address_validation/service' module Representatives # Processes updates for representative records based on provided JSON data. @@ -30,13 +32,18 @@ def perform(reps_json) # If the address validation fails or an error occurs during the update, the error is logged and the process # is halted for the current representative. # @param rep_data [Hash] The representative data including id and address. - def process_rep_data(rep_data) + def process_rep_data(rep_data) # rubocop:disable Metrics/MethodLength return unless record_can_be_updated?(rep_data) address_validation_api_response = nil if rep_data['address_changed'] - api_response = get_best_address_candidate(rep_data['address']) + + api_response = if Flipper.enabled?(:va_v3_contact_information_service) + get_best_address_candidate(rep_data) + else + get_best_address_candidate(rep_data['address']) + end # don't update the record if there is not a valid address with non-zero lat and long at this point if api_response.nil? @@ -67,13 +74,23 @@ def record_can_be_updated?(rep_data) # @param address [Hash] A hash containing the details of the representative's address. # @return [VAProfile::Models::ValidationAddress] A validation address object ready for address validation service. def build_validation_address(address) - VAProfile::Models::ValidationAddress.new( + if Flipper.enabled?(:va_v3_contact_information_service) + validation_model = VAProfile::Models::V3::ValidationAddress + state_code = address['state']['state_code'] + city = address['city_name'] + else + validation_model = VAProfile::Models::ValidationAddress + state_code = address['state_province']['code'] + city = address['city'] + end + + validation_model.new( address_pou: address['address_pou'], address_line1: address['address_line1'], address_line2: address['address_line2'], address_line3: address['address_line3'], - city: address['city'], - state_code: address['state_province']['code'], + city: city, + state_code: state_code, zip_code: address['zip_code5'], zip_code_suffix: address['zip_code4'], country_code_iso3: address['country_code_iso3'] @@ -84,7 +101,11 @@ def build_validation_address(address) # @param candidate_address [VAProfile::Models::ValidationAddress] The address to be validated. # @return [Hash] The response from the address validation service. def validate_address(candidate_address) - validation_service = VAProfile::AddressValidation::Service.new + validation_service = if Flipper.enabled?(:va_v3_contact_information_service) + VAProfile::V3::AddressValidation::Service.new + else + VAProfile::AddressValidation::Service.new + end validation_service.candidate(candidate_address) end @@ -102,7 +123,6 @@ def address_valid?(response) def update_rep_record(rep_data, api_response) record = Veteran::Service::Representative.find_by(representative_id: rep_data['id']) - if record.nil? raise StandardError, 'Representative not found.' else @@ -138,10 +158,14 @@ def update_flags(representative_id, flag_type) # @param rep_data [Hash] Original rep_data containing the address and other details. # @param api_response [Hash] The response from the address validation service. def build_address_attributes(rep_data, api_response) - address = api_response['candidate_addresses'].first['address'] - geocode = api_response['candidate_addresses'].first['geocode'] - meta = api_response['candidate_addresses'].first['address_meta_data'] - build_address(address, geocode, meta).merge({ raw_address: rep_data['address'].to_json }) + if Flipper.enabled?(:va_v3_contact_information_service) + build_v3_address(api_response['candidate_addresses'].first) + else + address = api_response['candidate_addresses'].first['address'] + geocode = api_response['candidate_addresses'].first['geocode'] + meta = api_response['candidate_addresses'].first['address_meta_data'] + build_address(address, geocode, meta).merge({ raw_address: rep_data['address'].to_json }) + end end def build_email_attributes(rep_data) @@ -182,6 +206,27 @@ def build_address(address, geocode, meta) } end + def build_v3_address(address) + { + address_type: address['address_type'], + address_line1: address['address_line1'], + address_line2: address['address_line2'], + address_line3: address['address_line3'], + city: address['city_name'], + province: address['state']['state_name'], + state_code: address['state']['state_code'], + zip_code: address['zip_code5'], + zip_suffix: address['zip_code4'], + country_code_iso3: address['country']['iso3_code'], + country_name: address['country']['country_name'], + county_name: address.dig('county', 'county_name'), + county_code: address.dig('county', 'county_code'), + lat: address['geocode']['latitude'], + long: address['geocode']['longitude'], + location: "POINT(#{address['geocode']['longitude']} #{address['geocode']['latitude']})" + } + end + # Logs an error to Sentry. # @param error [Exception] The error string to be logged. def log_error(error) diff --git a/modules/veteran/lib/tasks/update_vso_addresses.rake b/modules/veteran/lib/tasks/update_vso_addresses.rake index 5d87329c573..db84e76cff0 100644 --- a/modules/veteran/lib/tasks/update_vso_addresses.rake +++ b/modules/veteran/lib/tasks/update_vso_addresses.rake @@ -2,6 +2,8 @@ require 'va_profile/models/validation_address' require 'va_profile/address_validation/service' +require 'va_profile/models/v3/validation_address' +require 'va_profile/v3/address_validation/service' ADDRESS_BATCH1 = [ { @@ -902,8 +904,13 @@ ADDRESS_BATCH5 = [ # @param org [Hash] A hash containing the details of the organization's address. # @return [VAProfile::Models::ValidationAddress] A validation address object ready for address validation service. def build_validation_address(org) - VAProfile::Models::ValidationAddress.new( - address_pou: 'RESIDENCE/CHOICE', + validation_model = if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::Models::V3::ValidationAddress + else + VAProfile::Models::ValidationAddress + end + validation_model.new( + address_pou: org[:address_pou], address_line1: org[:address_line1], address_line2: org[:address_line2], address_line3: org[:address_line3], @@ -919,7 +926,11 @@ end # @param candidate_address [VAProfile::Models::ValidationAddress] The address to be validated. # @return [Hash] The response from the address validation service. def validate_address(candidate_address) - validation_service = VAProfile::AddressValidation::Service.new + validation_service = if Flipper.enabled?(:va_v3_contact_information_service, @current_user) + VAProfile::V3::AddressValidation::Service.new + else + VAProfile::AddressValidation::Service.new + end validation_service.candidate(candidate_address) end diff --git a/modules/veteran/spec/sidekiq/representatives/update_spec.rb b/modules/veteran/spec/sidekiq/representatives/update_spec.rb index 05612106eb1..54c61ca88b6 100644 --- a/modules/veteran/spec/sidekiq/representatives/update_spec.rb +++ b/modules/veteran/spec/sidekiq/representatives/update_spec.rb @@ -13,6 +13,7 @@ let(:address_exists) { true } before do + Flipper.disable(:va_v3_contact_information_service) create_flagged_records(flag_type) allow(VAProfile::AddressValidation::Service).to receive(:new).and_return(double('VAProfile::AddressValidation::Service', candidate: nil)) # rubocop:disable Layout/LineLength end @@ -48,6 +49,7 @@ let(:address_exists) { false } before do + Flipper.disable(:va_v3_contact_information_service) create_flagged_records(flag_type) end @@ -71,6 +73,46 @@ end end end + + context 'when address_exists is true for V3/AddressValidation' do + let(:address_exists) { true } + + before do + Flipper.enable(:va_v3_contact_information_service) + create_flagged_records(flag_type) + allow(VAProfile::V3::AddressValidation::Service).to receive(:new).and_return(double('VAProfile::V3::AddressValidation::Service', candidate: nil)) # rubocop:disable Layout/LineLength + end + + after do + Flipper.disable(:va_v3_contact_information_service) + end + + it "updates the #{flag_type} and the associated flagged records" do + flagged_records = + RepresentationManagement::FlaggedVeteranRepresentativeContactData + .where(representative_id: id, flag_type:) + + flagged_records.each do |record| + expect(record.flagged_value_updated_at).to be_nil + end + + subject.perform(json_data) + representative.reload + + expect(representative.send(attribute)).to eq(valid_value) + + flagged_records.each do |record| + record.reload + expect(record.flagged_value_updated_at).not_to be_nil + end + end + + it 'does not call validate_address or VAProfile::V3::AddressValidation::Service.new' do + subject.perform(json_data) + + expect(VAProfile::V3::AddressValidation::Service).not_to have_received(:new) + end + end end RSpec.describe Representatives::Update do @@ -181,6 +223,7 @@ def create_flagged_records(flag_type) end before do + Flipper.disable(:va_v3_contact_information_service) allow_any_instance_of(VAProfile::AddressValidation::Service).to receive(:candidate).and_return(api_response) end @@ -221,6 +264,7 @@ def create_flagged_records(flag_type) let!(:representative) { create_representative } before do + Flipper.disable(:va_v3_contact_information_service) create_flagged_records('address') end @@ -254,6 +298,7 @@ def create_flagged_records(flag_type) let!(:representative) { create_representative } before do + Flipper.disable(:va_v3_contact_information_service) create_flagged_records('address') end @@ -287,6 +332,7 @@ def create_flagged_records(flag_type) let!(:representative) { create_representative } before do + Flipper.disable(:va_v3_contact_information_service) create_flagged_records('address') end @@ -303,7 +349,6 @@ def create_flagged_records(flag_type) subject.perform(json_data) representative.reload - expect(representative.send('address_line1')).to eq('37N 1st St') expect(representative.send('email')).to eq('test@example.com') @@ -499,6 +544,7 @@ def create_flagged_records(flag_type) context 'when the first retry has non-zero coordinates' do before do + Flipper.disable(:va_v3_contact_information_service) allow(VAProfile::AddressValidation::Service).to receive(:new).and_return(validation_stub) allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response1) end @@ -519,6 +565,7 @@ def create_flagged_records(flag_type) context 'when the second retry has non-zero coordinates' do before do + Flipper.disable(:va_v3_contact_information_service) allow(VAProfile::AddressValidation::Service).to receive(:new).and_return(validation_stub) allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, api_response2) @@ -540,6 +587,7 @@ def create_flagged_records(flag_type) context 'when the third retry has non-zero coordinates' do before do + Flipper.disable(:va_v3_contact_information_service) allow(VAProfile::AddressValidation::Service).to receive(:new).and_return(validation_stub) allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, api_response_with_zero, api_response3) @@ -561,6 +609,7 @@ def create_flagged_records(flag_type) context 'when the retry coordinates are all zero' do before do + Flipper.disable(:va_v3_contact_information_service) allow(VAProfile::AddressValidation::Service).to receive(:new).and_return(validation_stub) allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, api_response_with_zero, api_response_with_zero) @@ -581,4 +630,487 @@ def create_flagged_records(flag_type) end end end + + describe 'V3/AddressValidation' do + # rubocop:disable Metrics/MethodLength + def create_representative + create(:representative, + representative_id: '123abc', + first_name: 'Bob', + last_name: 'Law', + address_line1: '123 East Main St', + address_line2: 'Suite 1', + address_line3: 'Address Line 3', + address_type: 'DOMESTIC', + city: 'My City', + country_name: 'United States of America', + country_code_iso3: 'USA', + province: 'A Province', + international_postal_code: '12345', + state_code: 'ZZ', + zip_code: '12345', + zip_suffix: '6789', + lat: '39', + long: '-75', + email: 'email@example.com', + location: 'POINT(-75 39)', + phone_number: '111-111-1111') + end + # rubocop:enable Metrics/MethodLength + + def create_flagged_records(flag_type) + 2.times do |n| + RepresentationManagement::FlaggedVeteranRepresentativeContactData.create( + ip_address: "192.168.1.#{n + 1}", + representative_id: '123abc', + flag_type:, + flagged_value: 'flagged_value' + ) + end + end + + describe '#perform V3/AddressValidation' do + let(:json_data) do + [ + { + id:, + address_pou: 'abc', + address_line1: 'abc', + address_line2: 'abc', + address_line3: 'abc', + city_name: 'abc', + state: { + state_code: 'abc' + }, + zip_code5: 'abc', + zip_code4: 'abc', + country_code_iso3: 'abc', + email: 'test@example.com', + phone_number: '999-999-9999', + address_exists:, + address_changed:, + email_changed:, + phone_number_changed: + } + ].to_json + end + let(:api_response) do + { + 'candidate_addresses' => [ + { + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'county_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_line1' => '37N 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + ] + } + end + + before do + Flipper.enable(:va_v3_contact_information_service) + allow_any_instance_of(VAProfile::V3::AddressValidation::Service).to receive(:candidate).and_return(api_response) + end + + context 'when JSON parsing fails' do + let(:invalid_json_data) { 'invalid json' } + + it 'logs an error to Sentry' do + expect_any_instance_of(SentryLogging).to receive(:log_message_to_sentry).with( + "Representatives::Update: Error processing job: unexpected token at 'invalid json'", :error + ) + + subject.perform(invalid_json_data) + end + end + + context 'when the representative cannot be found' do + let(:id) { 'not_found' } + let(:address_exists) { false } + let(:address_changed) { true } + let(:email_changed) { false } + let(:phone_number_changed) { false } + + it 'logs an error to Sentry' do + expect_any_instance_of(SentryLogging).to receive(:log_message_to_sentry).with( + 'Representatives::Update: Update failed for Rep id: not_found: Representative not found.', :error + ) + + subject.perform(json_data) + end + end + + context 'when address_exists is true and address_changed is true' do + let(:id) { '123abc' } + let(:address_exists) { true } + let(:address_changed) { true } + let(:email_changed) { false } + let(:phone_number_changed) { false } + let!(:representative) { create_representative } + + before do + create_flagged_records('address') + end + + it 'updates the address and the associated flagged records' do + flagged_records = + RepresentationManagement::FlaggedVeteranRepresentativeContactData + .where(representative_id: id, flag_type: 'address') + + flagged_records.each do |record| + expect(record.flagged_value_updated_at).to be_nil + end + + subject.perform(json_data) + representative.reload + + expect(representative.send('address_line1')).to eq('37N 1st St') + + flagged_records.each do |record| + record.reload + expect(record.flagged_value_updated_at).not_to be_nil + end + end + end + + context 'when address_exists is false and address_changed is true' do + let(:id) { '123abc' } + let(:address_exists) { false } + let(:address_changed) { true } + let(:email_changed) { false } + let(:phone_number_changed) { false } + let!(:representative) { create_representative } + + before do + create_flagged_records('address') + end + + it 'updates the address and the associated flagged records' do + flagged_records = + RepresentationManagement::FlaggedVeteranRepresentativeContactData + .where(representative_id: id, flag_type: 'address') + + flagged_records.each do |record| + expect(record.flagged_value_updated_at).to be_nil + end + + subject.perform(json_data) + representative.reload + + expect(representative.send('address_line1')).to eq('37N 1st St') + + flagged_records.each do |record| + record.reload + expect(record.flagged_value_updated_at).not_to be_nil + end + end + end + + context 'when address_changed and email_changed is true' do + let(:id) { '123abc' } + let(:address_exists) { false } + let(:address_changed) { true } + let(:email_changed) { true } + let(:phone_number_changed) { false } + let!(:representative) { create_representative } + + before do + Flipper.enable(:va_v3_contact_information_service) + create_flagged_records('address') + end + + it 'updates the address and email and the associated flagged records' do + flagged_address_records = + RepresentationManagement::FlaggedVeteranRepresentativeContactData + .where(representative_id: id, flag_type: 'address') + flagged_email_records = + RepresentationManagement::FlaggedVeteranRepresentativeContactData + .where(representative_id: id, flag_type: 'email') + flagged_email_records.each do |record| + expect(record.flagged_value_updated_at).to be_nil + end + + subject.perform(json_data) + representative.reload + expect(representative.send('address_line1')).to eq('37N 1st St') + expect(representative.send('email')).to eq('test@example.com') + + flagged_address_records + flagged_email_records.each do |record| + record.reload + expect(record.flagged_value_updated_at).not_to be_nil + end + end + end + + context "when updating a representative's email" do + it_behaves_like 'a representative email or phone update process', 'email', :email, 'test@example.com', + 'email@example.com' + end + + context "when updating a representative's phone number" do + it_behaves_like 'a representative email or phone update process', 'phone_number', :phone_number, '999-999-9999', + '111-111-1111' + end + + context 'address validation retries' do + let(:id) { '123abc' } + let(:address_exists) { true } + let(:address_changed) { true } + let(:email_changed) { false } + let(:phone_number_changed) { false } + let!(:representative) { create_representative } + let(:validation_stub) { instance_double(VAProfile::V3::AddressValidation::Service) } + let(:api_response_with_zero) do + { + 'candidate_addresses' => [ + { + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_line1' => '37N 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 0, + 'longitude' => 0 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + ] + } + end + let(:api_response1) do + { + 'candidate_addresses' => [ + { + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_line1' => '37N 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + ] + } + end + let(:api_response2) do + { + 'candidate_addresses' => [ + { + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_line1' => '37N 2nd St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + ] + } + end + let(:api_response3) do + { + 'candidate_addresses' => [ + { + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_line1' => '37N 3rd St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + ] + } + end + + context 'when the first retry has non-zero coordinates' do + before do + Flipper.enable(:va_v3_contact_information_service) + allow(VAProfile::V3::AddressValidation::Service).to receive(:new).and_return(validation_stub) + allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response1) + end + + it 'does not update the representative address' do + expect(representative.lat).to eq(39) + expect(representative.long).to eq(-75) + expect(representative.address_line1).to eq('123 East Main St') + + subject.perform(json_data) + representative.reload + + expect(representative.lat).to eq(40.717029) + expect(representative.long).to eq(-73.964956) + expect(representative.address_line1).to eq('37N 1st St') + end + end + + context 'when the second retry has non-zero coordinates' do + before do + Flipper.enable(:va_v3_contact_information_service) + allow(VAProfile::V3::AddressValidation::Service).to receive(:new).and_return(validation_stub) + allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, + api_response2) + end + + it 'does not update the representative address' do + expect(representative.lat).to eq(39) + expect(representative.long).to eq(-75) + expect(representative.address_line1).to eq('123 East Main St') + + subject.perform(json_data) + representative.reload + + expect(representative.lat).to eq(40.717029) + expect(representative.long).to eq(-73.964956) + expect(representative.address_line1).to eq('37N 2nd St') + end + end + + context 'when the third retry has non-zero coordinates' do + before do + Flipper.enable(:va_v3_contact_information_service) + allow(VAProfile::V3::AddressValidation::Service).to receive(:new).and_return(validation_stub) + allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, + api_response_with_zero, api_response3) + end + + it 'updates the representative address' do + expect(representative.lat).to eq(39) + expect(representative.long).to eq(-75) + expect(representative.address_line1).to eq('123 East Main St') + + subject.perform(json_data) + representative.reload + + expect(representative.lat).to eq(40.717029) + expect(representative.long).to eq(-73.964956) + expect(representative.address_line1).to eq('37N 3rd St') + end + end + + context 'when the retry coordinates are all zero' do + before do + Flipper.enable(:va_v3_contact_information_service) + allow(VAProfile::V3::AddressValidation::Service).to receive(:new).and_return(validation_stub) + allow(validation_stub).to receive(:candidate).and_return(api_response_with_zero, api_response_with_zero, + api_response_with_zero, api_response_with_zero) + end + + it 'does not update the representative address' do + expect(representative.lat).to eq(39) + expect(representative.long).to eq(-75) + expect(representative.address_line1).to eq('123 East Main St') + + subject.perform(json_data) + representative.reload + + expect(representative.lat).to eq(39) + expect(representative.long).to eq(-75) + expect(representative.address_line1).to eq('123 East Main St') + end + end + end + end + end end diff --git a/spec/factories/va_profile/v3/validation_address.rb b/spec/factories/va_profile/v3/validation_address.rb new file mode 100644 index 00000000000..b01bb10f81d --- /dev/null +++ b/spec/factories/va_profile/v3/validation_address.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'va_profile/models/v3/validation_address' + +FactoryBot.define do + factory :va_profile_v3_validation_address, class: 'VAProfile::Models::V3::ValidationAddress' do + address_pou { VAProfile::Models::V3::Address::RESIDENCE } + address_type { VAProfile::Models::V3::Address::DOMESTIC } + country_name { 'USA' } + country_code_iso3 { 'USA' } + + trait :multiple_matches do + address_line1 { '37 1st st' } + city { 'Brooklyn' } + state_code { 'NY' } + zip_code { '11249' } + end + + trait :override do + address_pou { VAProfile::Models::V3::Address::CORRESPONDENCE } + address_line1 { '1494 Martin Luther King Rd' } + address_line2 { 'c/o foo' } + city { 'Fulton' } + state_code { 'MS' } + zip_code { '38843' } + end + end +end diff --git a/spec/lib/va_profile/models/v3/validation_address_spec.rb b/spec/lib/va_profile/models/v3/validation_address_spec.rb new file mode 100644 index 00000000000..e2ffee98c3d --- /dev/null +++ b/spec/lib/va_profile/models/v3/validation_address_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'va_profile/models/v3/validation_address' + +describe VAProfile::Models::V3::ValidationAddress do + let(:address) { build(:va_profile_v3_validation_address, :multiple_matches) } + + describe '#address_validation_req' do + it 'formats the address for an address validation request' do + expect(address.address_validation_req).to eq( + address: { + 'addressLine1' => '37 1st st', + :cityName => 'Brooklyn', + :zipCode5 => '11249', + :country => { + countryCodeISO3: 'USA', + countryName: 'USA' + }, + :state => { + stateCode: 'NY' + }, + :province => {}, + :addressPOU => 'RESIDENCE' + } + ) + end + end + + describe '#build_from_address_suggestion' do + subject do + described_class.build_from_address_suggestion(address_suggestion_hash).to_h.compact + end + + context 'with a domestic address' do + let(:address_suggestion_hash) do + { + 'address_line1' => '37 N 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'addressPOU' => 'RESIDENCE', + 'county' => { 'county_name' => 'Kings', 'county_code' => '36047' }, + 'state' => { 'state_name' => 'New York', 'state_code' => 'NY' }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'geocode' => { + 'calc_date' => '2020-01-23T03:15:47+00:00', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + end + + it 'correctly parses the addresses' do + expect(subject).to eq( + { address_line1: '37 N 1st St', + address_type: 'DOMESTIC', + city: 'Brooklyn', + country_name: 'United States', + country_code_iso3: 'USA', + county_code: '36047', + county_name: 'Kings', + state_code: 'NY', + zip_code: '11249', + zip_code_suffix: '3939' } + ) + end + end + + context 'with an international address' do + let(:address_suggestion_hash) do + { + 'address_line1' => '898 Broadway W', + 'city_name' => 'Vancouver', + 'int_postal_code' => 'V5Z 1J8', + 'county' => {}, + 'province' => { + 'province_name' => 'British Columbia', + 'province_code' => 'BC' + }, + 'country' => { + 'country_name' => 'Canada', + 'country_code_fips' => 'CA', + 'country_code_iso2' => 'CA', + 'country_code_iso3' => 'CAN' + }, + 'geocode' => { + 'calc_date' => '2020-04-10T17:29:41Z', + 'location_precision' => 10.0, 'latitude' => 49.2635, + 'longitude' => -123.13873 + }, + 'confidence' => 97.76, + 'address_type' => 'International' + } + end + + it 'correctly parses international addresses' do + expect(subject).to eq( + { address_line1: '898 Broadway W', + address_type: 'INTERNATIONAL', + city: 'Vancouver', + country_name: 'Canada', + country_code_iso3: 'CAN', + international_postal_code: 'V5Z 1J8', + province: 'British Columbia' } + ) + end + end + end +end diff --git a/spec/lib/va_profile/v3/address_validation/service_spec.rb b/spec/lib/va_profile/v3/address_validation/service_spec.rb new file mode 100644 index 00000000000..e8bfe203787 --- /dev/null +++ b/spec/lib/va_profile/v3/address_validation/service_spec.rb @@ -0,0 +1,229 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'va_profile/v3/address_validation/service' + +describe VAProfile::V3::AddressValidation::Service do + let(:base_address) { build(:va_profile_v3_validation_address) } + + let(:address) do + base_address.address_line1 = '5 Stoddard Ct' + base_address.city = 'Sparks Glencoe' + base_address.state_code = 'MD' + base_address.zip_code = '21152' + + base_address + end + + let(:invalid_address) do + base_address.address_line1 = 'sdfdsfsdf' + base_address.city = 'Sparks Glencoe' + base_address.state_code = 'MD' + base_address.zip_code = '21152' + base_address + end + + let(:multiple_match_addr) do + build(:va_profile_v3_validation_address, :multiple_matches) + end + + before do + Flipper.enable(:va_v3_contact_information_service) + end + + after do + Flipper.disable(:va_v3_contact_information_service) + end + + describe '#address_suggestions' do + context 'with a found address' do + it 'returns suggested addresses' do + VCR.use_cassette( + 'va_profile/v3/address_validation/candidate_multiple_matches', + VCR::MATCH_EVERYTHING + ) do + res = described_class.new.address_suggestions(multiple_match_addr) + expect(JSON.parse(res.to_json)).to eq( + 'addresses' => [ + { + 'address' => { + 'address_line1' => '37 N 1st St', + 'address_type' => 'DOMESTIC', + 'city' => 'Brooklyn', + 'country_name' => 'United States', + 'country_code_iso3' => 'USA', + 'county_code' => '36047', + 'county_name' => 'Kings', + 'state_code' => 'NY', + 'zip_code' => '11249', + 'zip_code_suffix' => '3939' + }, + 'address_meta_data' => { + 'confidence_score' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + } + }, + { + 'address' => { + 'address_line1' => '37 S 1st St', + 'address_type' => 'DOMESTIC', + 'city' => 'Brooklyn', + 'country_name' => 'United States', + 'country_code_iso3' => 'USA', + 'county_code' => '36047', + 'county_name' => 'Kings', + 'state_code' => 'NY', + 'zip_code' => '11249', + 'zip_code_suffix' => '4101' + }, + 'address_meta_data' => { + 'confidence_score' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'CONFIRMED' + } + } + ], + 'validation_key' => '-646932106' + ) + end + end + end + end + + describe '#candidate' do + context 'with a request error' do + it 'raises backend service exception' do + allow_any_instance_of(described_class).to receive(:perform).and_raise(Common::Client::Errors::ClientError) + expect { described_class.new.candidate(invalid_address) }.to raise_error( + Common::Exceptions::BackendServiceException + ) + end + end + + context 'with an invalid address' do + it 'returns error messages' do + VCR.use_cassette( + 'va_profile/v3/address_validation/candidate_no_match', + VCR::MATCH_EVERYTHING + ) do + expect(described_class.new.candidate(invalid_address)).to eq( + 'candidate_addresses' => [ + { + 'address_line1' => 'Sdfdsfsdf', + 'city_name' => 'Sparks Glencoe', + 'zip_code5' => '21152', + 'state' => { + 'state_name' => 'Maryland', + 'state_code' => 'MD' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'geocode' => { + 'calc_date' => '2024-10-22T19:26:20+00:00Z', + 'latitude' => 39.5412, + 'longitude' => -76.6676 + }, + 'confidence' => 0.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'MISSING_ZIP' + } + ], + 'override_validation_key' => 1_499_210_293, + 'messages' => [ + { + 'code' => 'ADDRVAL108', + 'key' => 'CandidateAddressNotFound', + 'severity' => 'WARN', + 'text' => 'No Candidate Address Found', + 'potentially_self_correcting_on_retry' => true + } + ] + ) + end + end + end + + context 'with a found address' do + context 'with multiple matches' do + it 'returns suggested addresses for a given address' do + VCR.use_cassette( + 'va_profile/v3/address_validation/candidate_multiple_matches', + VCR::MATCH_EVERYTHING + ) do + res = described_class.new.candidate(multiple_match_addr) + expect(res).to eq( + 'candidate_addresses' => [ + { + 'address_line1' => '37 N 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '3939', + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_pou' => 'RESIDENCE', + 'geocode' => { + 'calc_date' => '2024-10-18T18:16:23.870Z', + 'location_precision' => 31.0, + 'latitude' => 40.717029, + 'longitude' => -73.964956 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'UNDELIVERABLE' + }, + { + 'address_line1' => '37 S 1st St', + 'city_name' => 'Brooklyn', + 'zip_code5' => '11249', + 'zip_code4' => '4101', + 'county' => { + 'county_name' => 'Kings', + 'county_code' => '36047' + }, + 'state' => { + 'state_name' => 'New York', + 'state_code' => 'NY' + }, + 'country' => { + 'country_name' => 'United States', + 'country_code_fips' => 'US', + 'country_code_iso2' => 'US', + 'country_code_iso3' => 'USA' + }, + 'address_pou' => 'RESIDENCE', + 'geocode' => { + 'calc_date' => '2024-10-18T18:16:23.870Z', + 'location_precision' => 31.0, + 'latitude' => 40.715367, + 'longitude' => -73.965369 + }, + 'confidence' => 100.0, + 'address_type' => 'Domestic', + 'delivery_point_validation' => 'CONFIRMED' + } + ], + 'override_validation_key' => '-646932106' + ) + end + end + end + end + end +end diff --git a/spec/requests/swagger_spec.rb b/spec/requests/swagger_spec.rb index 85878860df9..a36b46a8a2b 100644 --- a/spec/requests/swagger_spec.rb +++ b/spec/requests/swagger_spec.rb @@ -2916,13 +2916,13 @@ end it 'supports the address validation api' do - address = build(:va_profile_address, :multiple_matches) + address = build(:va_profile_v3_validation_address, :multiple_matches) VCR.use_cassette( 'va_profile/address_validation/validate_match', VCR::MATCH_EVERYTHING ) do VCR.use_cassette( - 'va_profile/address_validation/candidate_multiple_matches', + 'va_profile/v3/address_validation/candidate_multiple_matches', VCR::MATCH_EVERYTHING ) do expect(subject).to validate( diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f4c8ab427fc..a780d2c3059 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -33,6 +33,7 @@ add_filter 'lib/ihub/appointments/response.rb' add_filter 'lib/salesforce/configuration.rb' add_filter 'lib/va_profile/address_validation/configuration.rb' + add_filter 'lib/va_profile/v3/address_validation/configuration.rb' add_filter 'lib/search/response.rb' add_filter 'lib/search_gsa/response.rb' add_filter 'lib/va_profile/exceptions/builder.rb' diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb index 2dfacb6d0f7..a9820a4be58 100644 --- a/spec/support/vcr.rb +++ b/spec/support/vcr.rb @@ -39,6 +39,7 @@ c.filter_sensitive_data('') { Settings.vet360.url } c.filter_sensitive_data('') { Settings.form_10_10cg.carma.mulesoft.client_secret } c.filter_sensitive_data('') { Settings.vha.sharepoint.client_secret } + c.filter_sensitive_data('') { VAProfile::Configuration::SETTINGS.address_validation.url } c.filter_sensitive_data('') do Settings.lighthouse.benefits_education.rsa_key end diff --git a/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_multiple_matches.yml b/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_multiple_matches.yml new file mode 100644 index 00000000000..f5d33edba5e --- /dev/null +++ b/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_multiple_matches.yml @@ -0,0 +1,90 @@ +--- +http_interactions: +- request: + method: post + uri: /services/address-validation/v3/candidate + body: + encoding: UTF-8 + string: '{"address":{"addressLine1":"37 1st st","cityName":"Brooklyn","zipCode5":"11249","country":{"countryCodeISO3":"USA","countryName":"USA"},"state":{"stateCode":"NY"},"province":{},"addressPOU":"RESIDENCE"}}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Vets.gov Agent + Cufsystemname: + - VETSGOV + Apikey: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: '' + headers: + Date: + - Fri, 18 Oct 2024 18:31:28 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - openresty + Access-Control-Allow-Origin: + - "*" + Expires: + - '0' + Pragma: + - no-cache + - no-cache + Vet360txauditid: + - 836ed881-66db-47d5-9c89-02b65abdf20c + Via: + - kong/3.0.2 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Kong-Proxy-Latency: + - '2' + X-Kong-Upstream-Latency: + - '136' + X-Ratelimit-Limit-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '500' + X-Ratelimit-Limit-Minute: + - '60' + X-Ratelimit-Remaining-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '499' + X-Ratelimit-Remaining-Minute: + - '57' + X-Ratelimit-Reset-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '60000' + X-Ratelimit-Sla-Limit-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '250' + X-Ratelimit-Sla-Remaining-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '249' + X-Ua-Compatible: + - IE-edge,chrome=1 + X-Xss-Protection: + - 1; mode=block + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cache-Control: + - no-cache, no-store + body: + encoding: ASCII-8BIT + string: '{"candidateAddresses":[{"addressLine1":"37 N 1st St","cityName":"Brooklyn","zipCode5":"11249","zipCode4":"3939", + "state":{"stateName":"New York", "stateCode":"NY"},"country":{"countryName":"United States", "countryCodeFips":"US", + "countryCodeIso2":"US", "countryCodeIso3":"USA"},"addressPou":"RESIDENCE","county":{"countyName":"Kings", "countyCode":"36047"}, + "geocode":{"calcDate":"2024-10-18T18:16:23.870Z", "locationPrecision":31, "latitude":40.717029, "longitude":-73.964956}, + "deliveryPointValidation":"UNDELIVERABLE","addressType":"Domestic","confidence":100},{"addressLine1":"37 S 1st St","cityName":"Brooklyn","zipCode5":"11249","zipCode4":"4101", + "state":{"stateName":"New York", "stateCode":"NY"},"country":{"countryName":"United States", "countryCodeFips":"US", + "countryCodeIso2":"US", "countryCodeIso3":"USA"},"addressPou":"RESIDENCE","county":{"countyName":"Kings", "countyCode":"36047"}, + "geocode":{"calcDate":"2024-10-18T18:16:23.870Z", "locationPrecision":31, "latitude":40.715367, "longitude":-73.965369}, + "deliveryPointValidation":"CONFIRMED","addressType":"Domestic","confidence":100}],"overrideValidationKey":"-646932106"}' + recorded_at: Fri, 18 Oct 2024 18:31:28 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_no_match.yml b/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_no_match.yml new file mode 100644 index 00000000000..d6d493c0b8d --- /dev/null +++ b/spec/support/vcr_cassettes/va_profile/v3/address_validation/candidate_no_match.yml @@ -0,0 +1,89 @@ +--- +http_interactions: +- request: + method: post + uri: /services/address-validation/v3/candidate + body: + encoding: UTF-8 + string: '{"address":{"addressLine1":"sdfdsfsdf","cityName":"Sparks Glencoe","zipCode5":"21152","country":{"countryCodeISO3":"USA","countryName":"USA"},"state":{"stateCode":"MD"},"province":{},"addressPOU":"RESIDENCE"}}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Vets.gov Agent + Cufsystemname: + - VETSGOV + Apikey: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: '' + headers: + Date: + - Tue, 22 Oct 2024 19:26:20 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - openresty + Access-Control-Allow-Origin: + - "*" + Expires: + - '0' + Pragma: + - no-cache + - no-cache + Vet360txauditid: + - 1a19bb51-3466-4484-81bf-8c42585f91f1 + Via: + - kong/3.0.2 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Kong-Proxy-Latency: + - '2' + X-Kong-Upstream-Latency: + - '97' + X-Ratelimit-Limit-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '500' + X-Ratelimit-Limit-Minute: + - '60' + X-Ratelimit-Remaining-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '498' + X-Ratelimit-Remaining-Minute: + - '56' + X-Ratelimit-Reset-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '58751' + X-Ratelimit-Sla-Limit-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '250' + X-Ratelimit-Sla-Remaining-Edge-Gateway-Address-Validation-Vagovli-Address-V2-Candidate-Address: + - '248' + X-Ua-Compatible: + - IE-edge,chrome=1 + X-Xss-Protection: + - 1; mode=block + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cache-Control: + - no-cache, no-store + body: + encoding: ASCII-8BIT + string: '{"candidateAddresses":[{"addressLine1":"Sdfdsfsdf","cityName":"Sparks Glencoe","zipCode5":"21152", + "state":{"stateName":"Maryland","stateCode":"MD"},"country":{"countryName":"United States", + "countryCodeFips":"US","countryCodeIso2":"US","countryCodeIso3":"USA"},"geocode":{ + "calcDate":"2024-10-22T19:26:20+00:00Z","latitude":39.5412,"longitude":-76.6676},"confidence":0.0, + "addressType":"Domestic","deliveryPointValidation":"MISSING_ZIP"}],"overrideValidationKey":1499210293, + "messages":[{"code":"ADDRVAL108","key":"CandidateAddressNotFound","severity":"WARN","text":"No Candidate Address Found", + "potentially_self_correcting_on_retry":true}]}' + http_version: + recorded_at: Tue, 22 Oct 2024 19:26:20 GMT +recorded_with: VCR 6.3.1 From aa1801429406e78189c757db0253d1c41534155a Mon Sep 17 00:00:00 2001 From: Chris Kim <42885441+chriskim2311@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:19:45 -0800 Subject: [PATCH 2/2] VACMS-19545: add check for forms migration (#19254) --- modules/va_forms/app/sidekiq/va_forms/form_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/va_forms/app/sidekiq/va_forms/form_builder.rb b/modules/va_forms/app/sidekiq/va_forms/form_builder.rb index ff831fefa5d..49b2d8b9026 100644 --- a/modules/va_forms/app/sidekiq/va_forms/form_builder.rb +++ b/modules/va_forms/app/sidekiq/va_forms/form_builder.rb @@ -156,7 +156,7 @@ def gather_form_attributes(form_data) form_details_url: form_data['entityPublished'] ? "#{VAForms::Form::FORM_BASE_URL}#{form_data.dig('entityUrl', 'path')}" : '', form_tool_intro: form_data['fieldVaFormToolIntro'], - form_tool_url: form_data.dig('fieldVaFormToolUrl', 'uri'), + form_tool_url: form_data['entityPublished'] ? form_data.dig('fieldVaFormToolUrl', 'uri') : '', deleted_at: form_data.dig('fieldVaFormDeletedDate', 'value'), related_forms: form_data['fieldVaFormRelatedForms'].map { |f| f.dig('entity', 'fieldVaFormNumber') }, benefit_categories: parse_benefit_categories(form_data),