diff --git a/Gemfile.lock b/Gemfile.lock index c78452aaff6..85f49b65da3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -713,7 +713,7 @@ GEM parallel (1.24.0) parallel_tests (4.7.1) parallel - parser (3.3.0.5) + parser (3.3.1.0) ast (~> 2.4.1) racc patience_diff (1.2.0) @@ -920,8 +920,8 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) rubocop-capybara (2.20.0) rubocop (~> 1.41) rubocop-factory_bot (2.25.1) @@ -932,12 +932,12 @@ GEM rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.29.1) + rubocop-rspec (2.29.2) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.28.2) + rubocop-rspec_rails (2.28.3) rubocop (~> 1.40) rubocop-thread_safety (0.5.1) rubocop (>= 0.90.0) diff --git a/config/features.yml b/config/features.yml index 77f4da9f260..e17ed775e31 100644 --- a/config/features.yml +++ b/config/features.yml @@ -854,6 +854,9 @@ features: profile_show_quick_submit_notification_setting: actor_type: user description: Show/Hide the quick submit section of notification settings in profile + profile_show_proof_of_veteran_status: + actor_type: user + description: Show/Hide the proof of veteran status page and links profile_use_experimental: description: Use experimental features for Profile application - Do not remove enable_in_development: true diff --git a/config/form_profile_mappings/10-7959F-1.yml b/config/form_profile_mappings/10-7959F-1.yml index f9846dcb65e..25c47a31be4 100644 --- a/config/form_profile_mappings/10-7959F-1.yml +++ b/config/form_profile_mappings/10-7959F-1.yml @@ -1,22 +1,7 @@ -veteran: - date_of_birth: [identity_information, date_of_birth] - full_name: [identity_information, full_name] - first: [identity_information, first] - middle: [identity_information, middle] - last: [identity_information, last] -physical_address: - country: [contact_information, country] - street: [contact_information, street] - city: [contact_information, city] - state: [contact_information, state] - postal_code: [contact_information, postal_code] -mailing_address: - country: [contact_information, country] - street: [contact_information, street] - city: [contact_information, city] - state: [contact_information, state] - postal_code: [contact_information, postal_code] -ssn: [identity_information, ssn] -phone_number: [contact_information, us_phone] -email_address: [contact_information, email] \ No newline at end of file +veteranFullName: [identity_information, full_name] +veteranAddress: [contact_information, address] +veteranDateOfBirth: [identity_information, date_of_birth] +veteranSocialSecurityNumber: [identity_information, ssn] +veteranPhoneNumber: [contact_information, us_phone] +veteranEmailAddress: [contact_information, email] \ No newline at end of file diff --git a/modules/claims_api/README.md b/modules/claims_api/README.md index 53cd34fdb30..14bc40e63d2 100644 --- a/modules/claims_api/README.md +++ b/modules/claims_api/README.md @@ -10,9 +10,9 @@ ssh -L 4447:localhost:4447 {{aws-url}} ssh -L 4431:localhost:4431 {{aws-url}} ## Testing -### Unit testing BGS service operation wrappers +### Unit testing BGS service action wrappers If using cassettes, make sure to only make or use ones under [spec/support/vcr_cassettes/claims_api](spec/support/vcr_cassettes/claims_api) -Check out documentation in comments for the spec helper `BGSClientHelpers#use_bgs_cassette` +Check out documentation in comments for the spec helper `BGSClientSpecHelpers#use_bgs_cassette` ## OpenApi/Swagger Doc Generation This api uses [rswag](https://github.com/rswag/rswag) to build the OpenApi/Swagger docs that are displayed in the [VA|Lighthouse APIs Documentation](https://developer.va.gov/explore/benefits/docs/claims?version=current). To generate/update the docs for this api, navigate to the root directory of `vets-api` and run the following command :: diff --git a/modules/claims_api/app/clients/claims_api/bgs_client.rb b/modules/claims_api/app/clients/claims_api/bgs_client.rb new file mode 100644 index 00000000000..61e30c9bdec --- /dev/null +++ b/modules/claims_api/app/clients/claims_api/bgs_client.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module ClaimsApi + module BGSClient + class << self + ## + # Invokes the given BGS SOAP service action with the given payload and + # returns a result containing a success payload or a fault. + # + # @example Perform a request to BGS at: + # /VDC/ManageRepresentativeService(readPOARequest) + # + # body = <<~EOXML + # + # New + # + # + # 012 + # + # EOXML + # + # definition = + # BGSClient::ServiceAction::Definition:: + # ManageRepresentativeService:: + # ReadPoaRequest + # + # BGSClient.perform_request( + # definition:, + # body: + # ) + # + # @param definition [BGSClient::ServiceAction::Definition] a value object + # that identifies a particular BGS SOAP service action by way of: + # `{.service_path, .service_namespaces, .action_name}` + # + # @param body [String, #to_xml, #to_s] the action payload + # + # @param external_id [BGSClient::ServiceAction::ExternalId] a value object + # that arbitrarily self-identifies ourselves to BGS as its caller by: + # `{.external_uid, .external_key}` + # + # @return [BGSClient::ServiceAction::Request::Result] + # the response payload of a successful request, or the fault object of a + # failed request + def perform_request( + definition:, body:, + external_id: ServiceAction::ExternalId::DEFAULT + ) + ServiceAction + .const_get(:Request) + .new(definition:, external_id:) + .perform(body) + end + + ## + # Reveals the momentary health of a BGS service by attempting to request + # its WSDL and returning the HTTP status code of the response. + # + # @example + # definition = + # BGSClient::ServiceAction::Definition:: + # ManageRepresentativeService:: + # ReadPoaRequest + # + # BGSClient.healthcheck(definition) + # + # @param definition [BGSClient::ServiceAction::Definition] a value object + # that identifies a particular BGS SOAP service action by way of: + # `{.service_path, .service_namespaces, .action_name}` + # + # @return [Integer] HTTP status code + # + # @todo We could also introduce the notion of just the service definition + # in our central repository of definitions so that: + # 1. Service action definitions and other code would be able to refer to + # them + # 2. We could improve this API so that it doesn't need to receive + # extraneous action information. + # But this is fine for now. + def healthcheck(definition) + connection = build_connection + response = fetch_wsdl(connection, definition) + response.status + end + + def breakers_service + url = URI.parse(Settings.bgs.url) + request_matcher = + proc do |request_env| + request_env.url.host == url.host && + request_env.url.port == url.port && + request_env.url.path =~ /^#{url.path}/ + end + + Breakers::Service.new( + name: 'BGS/Claims', + request_matcher: + ) + end + + private + + def fetch_wsdl(connection, definition) + connection.get(definition.service_path) do |req| + req.params['WSDL'] = nil + end + end + + def build_connection + ssl_verify_mode = + if Settings.bgs.ssl_verify_mode == 'none' + OpenSSL::SSL::VERIFY_NONE + else + OpenSSL::SSL::VERIFY_PEER + end + + Faraday.new(Settings.bgs.url) do |conn| + conn.ssl.verify_mode = ssl_verify_mode + yield(conn) if block_given? + end + end + end + end +end diff --git a/modules/claims_api/app/clients/claims_api/bgs_client/service_action/definition.rb b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/definition.rb new file mode 100644 index 00000000000..4bbfa03cf35 --- /dev/null +++ b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/definition.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ClaimsApi + module BGSClient + module ServiceAction + class Definition < + Data.define( + :service_path, + :service_namespaces, + :action_name + ) + + module ManageRepresentativeService + service = { + service_path: 'VDC/ManageRepresentativeService', + service_namespaces: { 'data' => '/data' } + } + + ReadPoaRequest = + Definition.new( + action_name: 'readPOARequest', + **service + ) + end + end + end + end +end diff --git a/modules/claims_api/app/clients/claims_api/bgs_client/service_action/external_id.rb b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/external_id.rb new file mode 100644 index 00000000000..2509fc58386 --- /dev/null +++ b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/external_id.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ClaimsApi + module BGSClient + module ServiceAction + class ExternalId < Data.define(:external_uid, :external_key) + DEFAULT = + new( + external_uid: Settings.bgs.external_uid, + external_key: Settings.bgs.external_key + ) + end + end + end +end diff --git a/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request.rb b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request.rb new file mode 100644 index 00000000000..3cbff6e2811 --- /dev/null +++ b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'claims_api/claim_logger' + +module ClaimsApi + module BGSClient + module ServiceAction + # `private_constant` is used to prevent inheritance that could eventually + # tempt someone to add extraneous behavior to this, the parent class. + # Consumers should instead directly interface with + # `BGSClient.perform_request`, which maintains the sole responsibility of + # making a request to BGS. + private_constant :Request + + class Request + attr_reader :external_id + + def initialize(definition:, external_id:) + @definition = definition + @external_id = external_id + end + + def perform(body) # rubocop:disable Metrics/MethodLength + begin + wsdl = + log_duration('connection_wsdl_get') do + BGSClient.send( + :fetch_wsdl, + connection, + @definition + ).body + end + + request_body = + log_duration('built_request') do + wsdl_body = Hash.from_xml(wsdl) + namespace = wsdl_body.dig('definitions', 'targetNamespace').to_s + build_request_body(body, namespace:) + end + + response = + log_duration('connection_post') do + connection.post(@definition.service_path) do |req| + req.body = request_body + req.headers.merge!( + 'Soapaction' => %("#{@definition.action_name}"), + 'Content-Type' => 'text/xml;charset=UTF-8', + 'Host' => "#{Settings.bgs.env}.vba.va.gov" + ) + end + end + rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e + detail = "local BGS Faraday Timeout: #{e.message}" + ClaimsApi::Logger.log('local_bgs', retry: true, detail:) + raise ::Common::Exceptions::BadGateway + end + + log_duration('parsed_response') do + response_body = Hash.from_xml(response.body) + action_body = response_body.dig('Envelope', 'Body').to_h + fault = get_fault(action_body) + + if fault + Result.new( + success: false, + value: fault + ) + else + key = "#{@definition.action_name}Response" + value = action_body[key].to_h + + Result.new( + success: true, + value: + ) + end + end + end + + private + + def build_request_body(body, namespace:) # rubocop:disable Metrics/MethodLength + namespaces = + {}.tap do |value| + namespace = URI(namespace) + value['tns'] = namespace + + @definition.service_namespaces.to_h.each do |aliaz, path| + uri = namespace.clone + uri.path = path + value[aliaz] = uri + end + end + + client_ip = + if Rails.env.test? + # For all intents and purposes, BGS behaves identically no matter + # what IP we provide it. So in a test environment, let's just give + # it a fake IP so that cassette matching isn't defeated on CI and + # everyone's computer. + '127.0.0.1' + else + Socket + .ip_address_list + .detect(&:ipv4_private?) + .ip_address + end + + headers = + Envelope::Headers.new( + ip: client_ip, + username: Settings.bgs.client_username, + station_id: Settings.bgs.client_station_id, + application_name: Settings.bgs.application, + external_id: + ) + + action = @definition.action_name + + Envelope.generate( + namespaces:, + headers:, + action:, + body: + ) + end + + def get_fault(body) + fault = body['Fault'].to_h + return if fault.blank? + + message = + fault.dig('detail', 'MessageException') || + fault.dig('detail', 'MessageFaultException') + + Fault.new( + code: fault['faultcode'].to_s.split(':').last, + string: fault['faultstring'], + message: + ) + end + + def connection + @connection ||= + BGSClient.send(:build_connection) do |conn| + # Should all of this connection configuration really not be + # involved in the BGS service healthcheck performed by + # `BGSClient.healthcheck`? Under the hood, that just fetches WSDL + # which we also do here but with this more sophisticated logic. + # Maybe we truly don't want `breakers` and `timeout` logic to + # impact our assessment of service health in that context? + conn.use :breakers + conn.options.timeout = Settings.bgs.timeout || 120 + end + end + + # Use features of `SemanticLogger` like tags, metrics, benchmarking, + # appenders, etc rather than making bespoke implementations? + # https://logger.rocketjob.io/ + def log_duration(event_name) + start = now + result = yield + finish = now + + duration = (finish - start).round(4) + event = { + # event should be first key in log, duration last + event: event_name, + endpoint: @definition.service_path, + action: @definition.action_name, + duration: + } + + ClaimsApi::Logger.log('local_bgs', **event) + metric = "api.claims_api.local_bgs.#{event_name}.duration" + StatsD.measure(metric, duration, tags: {}) + + result + end + + def now + ::Process.clock_gettime( + ::Process::CLOCK_MONOTONIC + ) + end + + Fault = + Data.define( + :code, + :string, + :message + ) + + # Tiny subset of the API for `Dry::Monads[:result]`. Chose this + # particularly because some SOAP `500` really isn't error-like, and it + # is awkward to wrap exception handling for non-exceptional cases. + class Result + attr_reader :value + + def initialize(value:, success:) + @value = value + @success = success + end + + def success? + @success + end + + def failure? + !success? + end + end + end + end + end +end diff --git a/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request/envelope.rb b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request/envelope.rb new file mode 100644 index 00000000000..08bf04177b1 --- /dev/null +++ b/modules/claims_api/app/clients/claims_api/bgs_client/service_action/request/envelope.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module ClaimsApi + module BGSClient + module ServiceAction + class Request + module Envelope + Headers = + Data.define( + :ip, + :username, + :station_id, + :application_name, + :external_id + ) + + # rubocop:disable Style/FormatStringToken + TEMPLATE = <<~EOXML + + + + + + %{username} + + + %{ip} + %{station_id} + %{application_name} + %{external_uid} + %{external_key} + + + + + %{body} + + + EOXML + # rubocop:enable Style/FormatStringToken + + class << self + def generate(namespaces:, headers:, action:, body:) + namespaces = + namespaces.map do |aliaz, uri| + %(xmlns:#{aliaz}="#{uri}") + end + + headers = headers.to_h + external_id = headers.delete(:external_id).to_h + + format( + TEMPLATE, + namespaces: namespaces.join("\n"), + **headers, + **external_id, + action:, + body: + ) + end + end + end + end + end + end +end diff --git a/modules/claims_api/lib/bgs_service/local_bgs.rb b/modules/claims_api/lib/bgs_service/local_bgs.rb index 70cec2d24df..530bb62e046 100644 --- a/modules/claims_api/lib/bgs_service/local_bgs.rb +++ b/modules/claims_api/lib/bgs_service/local_bgs.rb @@ -212,8 +212,6 @@ def all(id) end # END: switching v1 from evss to bgs. Delete after EVSS is no longer available. Fix controller first. - private - def header # rubocop:disable Metrics/MethodLength # Stock XML structure {{{ header = Nokogiri::XML::DocumentFragment.parse <<~EOXML diff --git a/modules/claims_api/lib/bgs_service/local_bgs_proxy.rb b/modules/claims_api/lib/bgs_service/local_bgs_proxy.rb new file mode 100644 index 00000000000..07710b9d56a --- /dev/null +++ b/modules/claims_api/lib/bgs_service/local_bgs_proxy.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'bgs_service/local_bgs_refactored' +require 'bgs_service/local_bgs' + +module ClaimsApi + # Proxy class that switches at runtime between using `LocalBGS` and + # `LocalBGSRefactored` depending on the value of our feature toggle. + class LocalBGSProxy + legacy_ancestors = + LocalBGS.ancestors - + LocalBGSRefactored.ancestors + + legacy_api = + legacy_ancestors.flat_map do |ancestor| + ancestor.instance_methods(false) - [:initialize] + end + + refactored_ancestors = + LocalBGSRefactored.ancestors - + LocalBGS.ancestors + + refactored_api = + refactored_ancestors.flat_map do |ancestor| + ancestor.instance_methods(false) - [:initialize] + end + + # This makes the assumption that we'll maintain compatibility for callers of + # `LocalBGS` by considering only its public instance methods, and in + # particular those not installed by framework-level ancestors. A "one-time" + # check was performed to ensure that instance methods that callers invoke + # directly are contained in `common_api` and not contained in `missing_api`. + missing_api = legacy_api - refactored_api + common_api = legacy_api & refactored_api + + Rails.logger.trace( + "Comparison between LocalBGS and LocalBGSRefactored API's", + missing_api:, + common_api: + ) + + delegate(*common_api, to: :proxied) + attr_reader :proxied + + def initialize(...) + @proxied = + if Flipper.enabled?(:claims_api_local_bgs_refactor) + LocalBGSRefactored.new(...) + else + LocalBGS.new(...) + end + end + end +end diff --git a/modules/claims_api/lib/bgs_service/local_bgs_refactored.rb b/modules/claims_api/lib/bgs_service/local_bgs_refactored.rb new file mode 100644 index 00000000000..5d8199fdc6a --- /dev/null +++ b/modules/claims_api/lib/bgs_service/local_bgs_refactored.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# As a work of the United States Government, this project is in the +# public domain within the United States. +# +# Additionally, we waive copyright and related rights in the work +# worldwide through the CC0 1.0 Universal public domain dedication. + +require 'bgs_service/local_bgs_refactored/error_handler' +require 'bgs_service/local_bgs_refactored/miscellaneous' +require 'claims_api/claim_logger' + +module ClaimsApi + # @deprecated Use {BGSClient.perform_request} instead. There ought to be a + # clear separation between the single method that performs the transport to + # BGS and any business logic that invokes said transport. By housing that + # single method as an instance method of this class, we encouraged + # business logic modules to inherit this class and then inevitably start to + # conflate business logic back into the transport layer here. There was a + # particularly easy temptation to put business object state validation as + # well as the dumping and loading of business object state into this layer, + # but that should live in the business logic layer and not here. + class LocalBGSRefactored + include Miscellaneous + + attr_reader :external_id + + def initialize( + external_uid: Settings.bgs.external_uid, + external_key: Settings.bgs.external_key + ) + @external_id = + BGSClient::ServiceAction::ExternalId.new( + external_uid:, + external_key: + ) + end + + def healthcheck(endpoint) + definition = + BGSClient::ServiceAction::Definition.new( + service_path: endpoint, + service_namespaces: nil, + action_name: nil + ) + + BGSClient.healthcheck( + definition + ) + end + + def make_request( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists + endpoint:, action:, body:, key: nil, + namespaces: {}, transform_response: true + ) + definition = + BGSClient::ServiceAction::Definition.new( + service_path: endpoint, + service_namespaces: namespaces, + action_name: action + ) + + request = + BGSClient::ServiceAction.const_get(:Request).new( + definition:, + external_id: + ) + + result = request.perform(body) + + if result.success? + value = result.value.to_h + value = value[key].to_h if key.present? + + if transform_response + request.send(:log_duration, 'transformed_response') do + value.deep_transform_keys! do |key| + key.underscore.to_sym + end + end + end + + return value + end + + ErrorHandler.handle_errors!( + result.value + ) + + {} + end + end +end diff --git a/modules/claims_api/lib/bgs_service/local_bgs_refactored/error_handler.rb b/modules/claims_api/lib/bgs_service/local_bgs_refactored/error_handler.rb new file mode 100644 index 00000000000..2310b41620a --- /dev/null +++ b/modules/claims_api/lib/bgs_service/local_bgs_refactored/error_handler.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module ClaimsApi + class LocalBGSRefactored + # list of fault codes: https://hub.verj.io/ebase/doc/SOAP_Faults.htm + # + # TODO: Some (or all) of these cases should be handled in consumers and not + # in a central location. + class ErrorHandler + class << self + def handle_errors!(fault) + new(fault).handle_errors! + end + end + + def initialize(fault) + @fault = fault + end + + def handle_errors! + return if not_error? + + if not_found? + raise ::Common::Exceptions::ResourceNotFound.new(detail: 'Resource not found.') + elsif bnft_claim_not_found? + {} + elsif unprocessable? + raise ::Common::Exceptions::UnprocessableEntity.new( + detail: 'Please try again after checking your input values.' + ) + else + soap_logging('500') + raise ::Common::Exceptions::ServiceError.new(detail: 'An external server is experiencing difficulty.') + end + end + + private + + def not_error? + @fault.string.include?('IntentToFileWebService') && + @fault.string.include?('not found') + end + + def not_found? + errors = ['bnftClaimId-->bnftClaimId/text()', 'not found', 'No Person found'] + has_errors = errors.any? { |error| @fault.string.include? error } + soap_logging('404') if has_errors + has_errors + end + + def bnft_claim_not_found? + errors = ['No BnftClaim found'] + has_errors = errors.any? { |error| @fault.string.include? error } + soap_logging('404') if has_errors + has_errors + end + + def unprocessable? + errors = ['java.sql', 'MessageException', 'Validation errors', 'Exception Description', + 'does not have necessary info', 'Error committing transaction', 'Transaction Rolledback', + 'Unexpected error', 'XML reader error', 'could not be converted'] + has_errors = errors.any? { |error| @fault.string.include? error } + soap_logging('422') if has_errors + has_errors + end + + def soap_logging(status_code) + ClaimsApi::Logger.log('soap_error_handler', + detail: "Returning #{status_code} via local_bgs & soap_error_handler, " \ + "fault_string: #{@fault.string}, with message: #{@fault.message}, " \ + "and fault_code: #{@fault.code}.") + end + end + end +end diff --git a/modules/claims_api/lib/bgs_service/local_bgs_refactored/miscellaneous.rb b/modules/claims_api/lib/bgs_service/local_bgs_refactored/miscellaneous.rb new file mode 100644 index 00000000000..bc17d46b50c --- /dev/null +++ b/modules/claims_api/lib/bgs_service/local_bgs_refactored/miscellaneous.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'claims_api/evss_bgs_mapper' + +module ClaimsApi + class LocalBGSRefactored + module Miscellaneous # rubocop:disable Metrics/ModuleLength + def find_poa_by_participant_id(id) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { ptcpntId: id }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'ClaimantServiceBean/ClaimantWebService', action: 'findPOAByPtcpntId', body:, + key: 'return') + end + + def find_by_ssn(ssn) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { ssn: }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'PersonWebServiceBean/PersonWebService', action: 'findPersonBySSN', body:, + key: 'PersonDTO') + end + + def find_poa_history_by_ptcpnt_id(id) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { ptcpntId: id }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'OrgWebServiceBean/OrgWebService', action: 'findPoaHistoryByPtcpntId', body:, + key: 'PoaHistory') + end + + def find_benefit_claims_status_by_ptcpnt_id(id) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { ptcpntId: id }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'EBenefitsBnftClaimStatusWebServiceBean/EBenefitsBnftClaimStatusWebService', + action: 'findBenefitClaimsStatusByPtcpntId', body:) + end + + def claims_count(id) + find_benefit_claims_status_by_ptcpnt_id(id).count + rescue ::Common::Exceptions::ResourceNotFound + 0 + end + + def find_benefit_claim_details_by_benefit_claim_id(id) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { bnftClaimId: id }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'EBenefitsBnftClaimStatusWebServiceBean/EBenefitsBnftClaimStatusWebService', + action: 'findBenefitClaimDetailsByBnftClaimId', body:) + end + + def insert_intent_to_file(options) + request_body = construct_itf_body(options) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + + EOXML + + request_body.each do |k, z| + node = Nokogiri::XML::Node.new k.to_s, body + node.content = z.to_s + opt = body.at('intentToFileDTO') + node.parent = opt + end + make_request(endpoint: 'IntentToFileWebServiceBean/IntentToFileWebService', action: 'insertIntentToFile', + body:, key: 'IntentToFileDTO') + end + + def find_tracked_items(id) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + { claimId: id }.each do |k, v| + body.xpath("./*[local-name()='#{k}']")[0].content = v + end + + make_request(endpoint: 'TrackedItemService/TrackedItemService', action: 'findTrackedItems', body:, + key: 'BenefitClaim') + end + + def find_intent_to_file_by_ptcpnt_id_itf_type_cd(id, type) + body = Nokogiri::XML::DocumentFragment.parse <<~EOXML + + EOXML + + ptcpnt_id = body.at 'ptcpntId' + ptcpnt_id.content = id.to_s + itf_type_cd = body.at 'itfTypeCd' + itf_type_cd.content = type.to_s + + response = + make_request( + endpoint: 'IntentToFileWebServiceBean/IntentToFileWebService', + action: 'findIntentToFileByPtcpntIdItfTypeCd', + body: + ) + + Array.wrap(response[:intent_to_file_dto]) + end + + # BEGIN: switching v1 from evss to bgs. Delete after EVSS is no longer available. Fix controller first. + def update_from_remote(id) + bgs_claim = find_benefit_claim_details_by_benefit_claim_id(id) + transform_bgs_claim_to_evss(bgs_claim) + end + + def all(id) + claims = find_benefit_claims_status_by_ptcpnt_id(id) + return [] if claims.count < 1 || claims[:benefit_claims_dto].blank? + + transform_bgs_claims_to_evss(claims) + end + # END: switching v1 from evss to bgs. Delete after EVSS is no longer available. Fix controller first. + + def construct_itf_body(options) + request_body = { + itfTypeCd: options[:intent_to_file_type_code], + ptcpntVetId: options[:participant_vet_id], + rcvdDt: options[:received_date], + signtrInd: options[:signature_indicated], + submtrApplcnTypeCd: options[:submitter_application_icn_type_code] + } + request_body[:ptcpntClmantId] = options[:participant_claimant_id] if options.key?(:participant_claimant_id) + request_body[:clmantSsn] = options[:claimant_ssn] if options.key?(:claimant_ssn) + request_body + end + + def transform_bgs_claim_to_evss(claim) + bgs_claim = ClaimsApi::EvssBgsMapper.new(claim[:benefit_claim_details_dto]) + return if bgs_claim.nil? + + bgs_claim.map_and_build_object + end + + def transform_bgs_claims_to_evss(claims) + claims_array = [claims[:benefit_claims_dto][:benefit_claim]].flatten + claims_array&.map do |claim| + bgs_claim = ClaimsApi::EvssBgsMapper.new(claim) + bgs_claim.map_and_build_object + end + end + + def convert_nil_values(options) + arg_strg = '' + options.each do |option| + arg = option[0].to_s.camelize(:lower) + arg_strg += (option[1].nil? ? "<#{arg} xsi:nil='true'/>" : "<#{arg}>#{option[1]}") + end + arg_strg + end + + def validate_opts!(opts, required_keys) + keys = opts.keys.map(&:to_s) + required_keys = required_keys.map(&:to_s) + missing_keys = required_keys - keys + raise ArgumentError, "Missing required keys: #{missing_keys.join(', ')}" if missing_keys.present? + end + + def jrn + { + jrn_dt: Time.current.iso8601, + jrn_lctn_id: Settings.bgs.client_station_id, + jrn_status_type_cd: 'U', + jrn_user_id: Settings.bgs.client_username, + jrn_obj_id: Settings.bgs.application + } + end + + def to_camelcase(claim:) + claim.deep_transform_keys { |k| k.to_s.camelize(:lower) } + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/create_veteran_representative_request_spec.rb b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/create_veteran_representative_request_spec.rb index ebcf7905201..6b4a039e1cf 100644 --- a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/create_veteran_representative_request_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/create_veteran_representative_request_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/veteran_representative_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'veteran_representative_service', - operation: 'create_veteran_representative' + action: 'create_veteran_representative' } } diff --git a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/read_all_veteran_representatives_spec.rb b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/read_all_veteran_representatives_spec.rb index cdbabb597e4..4bdcde0fdbf 100644 --- a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/read_all_veteran_representatives_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/read_all_veteran_representatives_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/veteran_representative_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'veteran_representative_service', - operation: 'read_all_veteran_representatives' + action: 'read_all_veteran_representatives' }, run_at: '2024-04-17T23:10:31+00:00' } diff --git a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/veteran_representative_service_spec.rb b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/veteran_representative_service_spec.rb index c2db3c02fd1..5a35a2cacad 100644 --- a/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/veteran_representative_service_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/bgs/veteran_representative_service/veteran_representative_service_spec.rb @@ -2,7 +2,6 @@ require 'rails_helper' require 'bgs_service/veteran_representative_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') describe ClaimsApi::VeteranRepresentativeService do let(:header_params) do diff --git a/modules/claims_api/spec/lib/claims_api/local_bgs_proxy_spec.rb b/modules/claims_api/spec/lib/claims_api/local_bgs_proxy_spec.rb new file mode 100644 index 00000000000..f016cfa02ca --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/local_bgs_proxy_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/local_bgs_proxy' + +describe ClaimsApi::LocalBGSProxy do + subject do + described_class.new( + external_uid: nil, + external_key: nil + ) + end + + expected_instance_methods = { + all: %i[id], + claims_count: %i[id], + construct_itf_body: %i[options], + convert_nil_values: %i[options], + find_benefit_claim_details_by_benefit_claim_id: %i[id], + find_benefit_claims_status_by_ptcpnt_id: %i[id], + find_by_ssn: %i[ssn], + find_intent_to_file_by_ptcpnt_id_itf_type_cd: %i[id type], + find_poa_by_participant_id: %i[id], + find_poa_history_by_ptcpnt_id: %i[id], + find_tracked_items: %i[id], + healthcheck: %i[endpoint], + insert_intent_to_file: %i[options], + jrn: %i[], + make_request: [endpoint: nil, action: nil, body: nil], + to_camelcase: [claim: nil], + transform_bgs_claim_to_evss: %i[claim], + transform_bgs_claims_to_evss: %i[claims], + update_from_remote: %i[id], + validate_opts!: %i[opts required_keys] + } + + expected_instance_methods.each_value(&:freeze) + expected_instance_methods.freeze + + it 'defines the correct set of instance methods' do + actual = described_class.instance_methods(false) - [:proxied] + expect(actual).to match_array(expected_instance_methods.keys) + end + + describe 'claims_api_local_bgs_refactor feature toggling' do + before do + expect(Flipper).to( + receive(:enabled?) + .with(:claims_api_local_bgs_refactor) + .and_return(toggle) + ) + end + + define_singleton_method(:it_delegates_every_instance_method) do |to:| + it "has a proxied of type #{to}" do + expect(subject.proxied).to be_a(to) + end + + expected_instance_methods.each do |meth, args| + describe "when instance method is `#{meth}`" do + it "delegates to `#{to}`" do + if args.empty? + expect(subject.proxied).to receive(meth).with(no_args).once + subject.send(meth) + else + args = args.deep_dup + kwargs = args.extract_options! + expect(subject.proxied).to receive(meth).with(*args, **kwargs).once + subject.send(meth, *args, **kwargs) + end + end + end + end + end + + describe 'with refactor toggled off' do + let(:toggle) { false } + + it_delegates_every_instance_method( + to: ClaimsApi::LocalBGS + ) + end + + describe 'with refactor toggled on' do + let(:toggle) { true } + + it_delegates_every_instance_method( + to: ClaimsApi::LocalBGSRefactored + ) + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/local_bgs_refactored_spec.rb b/modules/claims_api/spec/lib/claims_api/local_bgs_refactored_spec.rb new file mode 100644 index 00000000000..bd0bb5c4689 --- /dev/null +++ b/modules/claims_api/spec/lib/claims_api/local_bgs_refactored_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'bgs_service/local_bgs_proxy' + +describe ClaimsApi::LocalBGSProxy do + subject { described_class.new external_uid: 'xUid', external_key: 'xKey' } + + before do + allow(Flipper).to( + receive(:enabled?) + .with(:claims_api_local_bgs_refactor) + .and_return(true) + ) + end + + let(:soap_error_handler) { ClaimsApi::LocalBGSRefactored::ErrorHandler } + + describe '#find_poa_by_participant_id' do + it 'responds as expected, with extra ClaimsApi::Logger logging' do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id') do + # Events logged: + # 1: connection_wsdl_get - duration of WSDL request cycle + # 2: built_request - how long to build the request + # 3: connection_post - how long does the post itself take for the request cycle + # 4: parsed_response - how long to parse the response + # 5: transformed_response - how long to transform the response + expect(ClaimsApi::Logger).to receive(:log).exactly(5).times + result = subject.find_poa_by_participant_id('does-not-matter') + expect(result).to be_a Hash + expect(result[:end_date]).to eq '08/26/2020' + end + end + + describe 'breakers' do + it 'returns a Bad Gateway' do + stub_request(:any, "#{Settings.bgs.url}/ClaimantServiceBean/ClaimantWebService?WSDL").to_timeout + expect do + subject.find_poa_by_participant_id('also-does-not-matter') + end.to raise_error(Common::Exceptions::BadGateway) + end + + it 'hits breakers' do + ClaimsApi::BGSClient.breakers_service.begin_forced_outage! + expect { subject.find_poa_by_participant_id('also-does-not-matter') }.to raise_error(Breakers::OutageException) + ClaimsApi::BGSClient.breakers_service.end_forced_outage! + end + end + + it 'triggers StatsD measurements' do + VCR.use_cassette('claims_api/bgs/claimant_web_service/find_poa_by_participant_id', + allow_playback_repeats: true) do + %w[connection_wsdl_get built_request connection_post parsed_response transformed_response].each do |event| + expect { subject.find_poa_by_participant_id('does-not-matter') } + .to trigger_statsd_measure("api.claims_api.local_bgs.#{event}.duration") + end + end + end + end + + # Testing potential ways the current check could be tricked + describe '#all' do + let(:subject_instance) { subject } + let(:id) { 12_343 } + let(:error_message) { { error: 'Did not work', code: 'XXX' } } + let(:bgs_unknown_error_message) { { error: 'Unexpected error' } } + let(:empty_array) { [] } + + context 'when an error message gets returned it still does not pass the count check' do + it 'returns an empty array' do + expect(error_message.count).to eq(2) # trick the claims count check + # error message should trigger return + allow(subject_instance.proxied).to( + receive(:find_benefit_claims_status_by_ptcpnt_id).with(id).and_return(error_message) + ) + expect(subject.all(id)).to eq([]) # verify correct return + end + end + + context 'when claims come back as a hash instead of an array' do + it 'casts the hash as an array' do + VCR.use_cassette('claims_api/bgs/claims/claims_trimmed_down') do + claims = subject_instance.find_benefit_claims_status_by_ptcpnt_id('600061742') + claims[:benefit_claims_dto][:benefit_claim] = claims[:benefit_claims_dto][:benefit_claim][0] + allow(subject_instance.proxied).to( + receive(:find_benefit_claims_status_by_ptcpnt_id).with(id).and_return(claims) + ) + + begin + ret = subject_instance.send(:transform_bgs_claims_to_evss, claims) + expect(ret.class).to_be Array + expect(ret.size).to eq 1 + rescue => e + expect(e.message).not_to include 'no implicit conversion of Array into Hash' + end + end + end + end + + # Already being checked but based on an error seen just want to lock this in to ensure nothing gets missed + context 'when an empty array gets returned it still does not pass the count check' do + it 'returns an empty array' do + # error message should trigger return + allow(subject_instance.proxied).to( + receive(:find_benefit_claims_status_by_ptcpnt_id).with(id).and_return(empty_array) + ) + expect(subject.all(id)).to eq([]) # verify correct return + end + end + + context 'when an error message gets returns unknown' do + it 'the soap error handler returns unprocessable' do + allow(subject_instance).to receive(:make_request).with(endpoint: 'PersonWebServiceBean/PersonWebService', + action: 'findPersonBySSN', + body: Nokogiri::XML::DocumentFragment.new( + Nokogiri::XML::Document.new + ), + key: 'PersonDTO').and_return(:bgs_unknown_error_message) + begin + allow(soap_error_handler).to receive(:handle_errors!) + .with(:bgs_unknown_error_message).and_raise(Common::Exceptions::UnprocessableEntity) + ret = soap_error_handler.send(:handle_errors!, :bgs_unknown_error_message) + expect(ret.class).to_be Array + expect(ret.size).to eq 1 + rescue => e + expect(e.message).to include 'Unprocessable Entity' + end + end + end + end +end diff --git a/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb b/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb index 24ba183322a..af695f4a26c 100644 --- a/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/manage_representative_service/read_poa_request_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/manage_representative_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'manage_representative_service', - operation: 'read_poa_request' + action: 'read_poa_request' } } diff --git a/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb b/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb index 525f504f95b..680e7acf9b9 100644 --- a/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/manage_representative_service/update_poa_request_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/manage_representative_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'manage_representative_service', - operation: 'update_poa_request' + action: 'update_poa_request' } } diff --git a/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb index 6260c7993a6..96e078dbde3 100644 --- a/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/vnp_atchms_service_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/vnp_atchms_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'vnp_atchms_service', - operation: 'vnp_atchms_create' + action: 'vnp_atchms_create' } } diff --git a/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb index 47acbb2681c..9cf16770b8c 100644 --- a/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/vnp_person_service_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/vnp_person_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'vnp_person_service', - operation: 'vnp_person_create' + action: 'vnp_person_create' } } diff --git a/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_addrs_service_spec.rb b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_addrs_service_spec.rb index e9dfa8550cb..7a626a44c3f 100644 --- a/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_addrs_service_spec.rb +++ b/modules/claims_api/spec/lib/claims_api/vnp_ptcpnt_addrs_service_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' require 'bgs_service/vnp_ptcpnt_addrs_service' -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { bgs: { service: 'vnp_ptcpnt_addrs_service', - operation: 'vnp_ptcpnt_addrs_create' + action: 'vnp_ptcpnt_addrs_create' } } diff --git a/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb b/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb index b4125980b5e..fa1816989a4 100644 --- a/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb +++ b/modules/claims_api/spec/requests/v2/power_of_attorney_requests/index/rswag_spec.rb @@ -4,14 +4,14 @@ require Rails.root.join('spec', 'rswag_override.rb').to_s require 'rails_helper' require Rails.root.join('modules', 'claims_api', 'spec', 'rails_helper.rb') -require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_helpers.rb') +require Rails.root.join('modules', 'claims_api', 'spec', 'support', 'bgs_client_spec_helpers.rb') metadata = { openapi_spec: Rswag::TextHelpers.new.claims_api_docs, production: false, bgs: { service: 'manage_representative_service', - operation: 'read_poa_request' + action: 'read_poa_request' } } diff --git a/modules/claims_api/spec/support/bgs_client_helpers.rb b/modules/claims_api/spec/support/bgs_client_spec_helpers.rb similarity index 74% rename from modules/claims_api/spec/support/bgs_client_helpers.rb rename to modules/claims_api/spec/support/bgs_client_spec_helpers.rb index 4d6394c50fd..884d28f3c01 100644 --- a/modules/claims_api/spec/support/bgs_client_helpers.rb +++ b/modules/claims_api/spec/support/bgs_client_spec_helpers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module BGSClientHelpers +module BGSClientSpecHelpers # If one finds this request matcher useful elsewhere in the future, # Rather than using a callable custom request matcher: # https://benoittgt.github.io/vcr/#/request_matching/custom_matcher?id=use-a-callable-as-a-custom-request-matcher @@ -19,44 +19,53 @@ module BGSClientHelpers end VCR_OPTIONS = { - # Consider matching on `:headers` too? match_requests_on: [ - :method, :uri, + :method, :uri, :headers, body_as_xml_matcher.freeze ].freeze }.freeze # This convenience method affords a handful of quality of life improvements - # for developing BGS service operation wrappers. It makes development a less + # for developing BGS service action wrappers. It makes development a less # manual process. It also turns VCR cassettes into a human readable resource # that documents the behavior of BGS. # # In order to take advantage of this method, you will need to have supplied, # to your example or example group, metadata of this form: - # `{ bgs: { service: "service", operation: "operation" } }`. + # `{ bgs: { service: "service", action: "action" } }`. # # Then, HTTP interactions that occur within the block supplied to this method # will be captured by VCR cassettes that have the following convenient # properties: - # - They will be nicely organized at `claims_api/bgs/:service/:operation/:name` + # - They will be nicely organized at `claims_api/bgs/:service/:action/:name` # - Cassette matching will be done on canonicalized XML bodies, so # reformatting cassettes for human readability won't defeat matching def use_bgs_cassette(name, &) metadata = RSpec.current_example.metadata[:bgs].to_h - service, operation = metadata.values_at(:service, :operation) + service, action = metadata.values_at(:service, :action) - if service.blank? || operation.blank? + if service.blank? || action.blank? raise ArgumentError, <<~HEREDOC Must provide spec metadata of the form: - `{ bgs: { service: "service", operation: "operation" } }' + `{ bgs: { service: "service", action: "action" } }' HEREDOC end - name = File.join('claims_api/bgs', service, operation, name) + name = File.join('claims_api/bgs', service, action, name) VCR.use_cassette(name, VCR_OPTIONS, &) end end RSpec.configure do |config| - config.include BGSClientHelpers, :bgs + config.include BGSClientSpecHelpers, :bgs + + unless Settings.bgs.refactor.nil? + config.before(:example, :bgs) do + allow(Flipper).to( + receive(:enabled?) + .with(:claims_api_local_bgs_refactor) + .and_return(Settings.bgs.refactor) + ) + end + end end diff --git a/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb b/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb index a34f2dfb283..9a033b26359 100644 --- a/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb +++ b/modules/simple_forms_api/app/controllers/simple_forms_api/v1/uploads_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true require 'ddtrace' -require 'simple_forms_api_submission/service' require 'simple_forms_api_submission/metadata_validator' -require 'simple_forms_api_submission/s3' require 'lgy/service' module SimpleFormsApi @@ -112,7 +110,8 @@ def submit_form_to_central_mail parsed_form_data = JSON.parse(params.to_json) file_path, metadata, form = get_file_paths_and_metadata(parsed_form_data) - status, confirmation_number = upload_pdf_to_benefits_intake(file_path, metadata, form_id) + status, confirmation_number = SimpleFormsApi::PdfUploader.new(file_path, metadata, + form_id).upload_to_benefits_intake(params) form.track_user_identity(confirmation_number) Rails.logger.info( @@ -147,43 +146,6 @@ def get_file_paths_and_metadata(parsed_form_data) [file_path, metadata, form] end - def get_upload_location_and_uuid(lighthouse_service, form_id) - upload_location = lighthouse_service.get_upload_location.body - if form_id == 'vba_40_10007' - uuid = upload_location.dig('data', 'id') - SimpleFormsApi::PdfStamper.stamp4010007_uuid(uuid) - end - { - uuid: upload_location.dig('data', 'id'), - location: upload_location.dig('data', 'attributes', 'location') - } - end - - def upload_pdf_to_benefits_intake(file_path, metadata, form_id) - lighthouse_service = SimpleFormsApiSubmission::Service.new - uuid_and_location = get_upload_location_and_uuid(lighthouse_service, form_id) - form_submission = FormSubmission.create( - form_type: params[:form_number], - benefits_intake_uuid: uuid_and_location[:uuid], - form_data: params.to_json, - user_account: @current_user&.user_account - ) - FormSubmissionAttempt.create(form_submission:) - - Datadog::Tracing.active_trace&.set_tag('uuid', uuid_and_location[:uuid]) - Rails.logger.info( - 'Simple forms api - preparing to upload PDF to benefits intake', - { location: uuid_and_location[:location], uuid: uuid_and_location[:uuid] } - ) - response = lighthouse_service.upload_doc( - upload_url: uuid_and_location[:location], - file: file_path, - metadata: metadata.to_json - ) - - [response.status, uuid_and_location[:uuid]] - end - def form_is210966 params[:form_number] == '21-0966' end diff --git a/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb b/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb new file mode 100644 index 00000000000..40ed1d8179b --- /dev/null +++ b/modules/simple_forms_api/app/services/simple_forms_api/pdf_uploader.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'simple_forms_api_submission/service' + +module SimpleFormsApi + class PdfUploader + attr_reader :file_path, :metadata, :form_id + + def initialize(file_path, metadata, form_id) + @file_path = file_path + @metadata = metadata + @form_id = form_id + end + + def upload_to_benefits_intake(params) + lighthouse_service = SimpleFormsApiSubmission::Service.new + uuid_and_location = get_upload_location_and_uuid(lighthouse_service, form_id) + form_submission = FormSubmission.create( + form_type: params[:form_number], + benefits_intake_uuid: uuid_and_location[:uuid], + form_data: params.to_json, + user_account: @current_user&.user_account + ) + FormSubmissionAttempt.create(form_submission:) + + Datadog::Tracing.active_trace&.set_tag('uuid', uuid_and_location[:uuid]) + Rails.logger.info( + 'Simple forms api - preparing to upload PDF to benefits intake', + { location: uuid_and_location[:location], uuid: uuid_and_location[:uuid] } + ) + response = lighthouse_service.upload_doc( + upload_url: uuid_and_location[:location], + file: file_path, + metadata: metadata.to_json + ) + + [response.status, uuid_and_location[:uuid]] + end + + private + + def get_upload_location_and_uuid(lighthouse_service, form_id) + upload_location = lighthouse_service.get_upload_location.body + if form_id == 'vba_40_10007' + uuid = upload_location.dig('data', 'id') + SimpleFormsApi::PdfStamper.stamp4010007_uuid(uuid) + end + { + uuid: upload_location.dig('data', 'id'), + location: upload_location.dig('data', 'attributes', 'location') + } + end + end +end diff --git a/modules/simple_forms_api/spec/requests/v1/uploads_spec.rb b/modules/simple_forms_api/spec/requests/v1/uploads_spec.rb index a9c7d84b2cd..abdc9dd939c 100644 --- a/modules/simple_forms_api/spec/requests/v1/uploads_spec.rb +++ b/modules/simple_forms_api/spec/requests/v1/uploads_spec.rb @@ -516,8 +516,8 @@ it 'successful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([200, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([200, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -536,8 +536,8 @@ it 'unsuccessful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([500, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([500, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -557,8 +557,8 @@ it 'successful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([200, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([200, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -577,8 +577,8 @@ it 'unsuccessful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([500, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([500, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -604,11 +604,8 @@ it 'successful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of( - SimpleFormsApi::V1::UploadsController - ).to receive( - :upload_pdf_to_benefits_intake - ).and_return([200, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([200, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -628,11 +625,8 @@ it 'unsuccessful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of( - SimpleFormsApi::V1::UploadsController - ).to receive( - :upload_pdf_to_benefits_intake - ).and_return([500, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([500, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -652,8 +646,8 @@ it 'successful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([200, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([200, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data @@ -672,8 +666,8 @@ it 'unsuccessful submission' do allow(VANotify::EmailJob).to receive(:perform_async) - allow_any_instance_of(SimpleFormsApi::V1::UploadsController) - .to receive(:upload_pdf_to_benefits_intake).and_return([500, confirmation_number]) + allow_any_instance_of(SimpleFormsApi::PdfUploader) + .to receive(:upload_to_benefits_intake).and_return([500, confirmation_number]) post '/simple_forms_api/v1/simple_forms', params: data diff --git a/modules/simple_forms_api/spec/services/pdf_uploader_spec.rb b/modules/simple_forms_api/spec/services/pdf_uploader_spec.rb new file mode 100644 index 00000000000..afdaf7b8d58 --- /dev/null +++ b/modules/simple_forms_api/spec/services/pdf_uploader_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' +require SimpleFormsApi::Engine.root.join('spec', 'spec_helper.rb') +require 'simple_forms_api_submission/service' + +describe SimpleFormsApi::PdfUploader do + describe '#upload_to_benefits_intake' do + it 'returns the status and uuid from Lighthouse' do + file_path = '/some-path' + metadata = { 'meta' => 'data' } + form_id = '12-3456' + expected_status = 200 + expected_uuid = 'some-uuid' + lighthouse_service = double + upload_location = double + body = { 'data' => { 'id' => expected_uuid } } + params = { form_number: form_id } + expected_response = double(status: expected_status) + allow(upload_location).to receive(:body).and_return body + allow(lighthouse_service).to receive(:get_upload_location).and_return upload_location + allow(lighthouse_service).to receive(:upload_doc).and_return expected_response + allow(SimpleFormsApiSubmission::Service).to receive(:new).and_return lighthouse_service + + pdf_uploader = SimpleFormsApi::PdfUploader.new(file_path, metadata, form_id) + + expect(pdf_uploader.upload_to_benefits_intake(params)).to eq [expected_status, expected_uuid] + end + end +end diff --git a/spec/models/form_profile_spec.rb b/spec/models/form_profile_spec.rb index f6c9d2ddac8..c7e16b972eb 100644 --- a/spec/models/form_profile_spec.rb +++ b/spec/models/form_profile_spec.rb @@ -1815,5 +1815,20 @@ def expect_prefilled(form_id) instance2.prefill end end + + context '10-7959F-1 form profile instances' do + let(:instance) { FormProfile.new(form_id: '10-7959F-1', user:) } + + it 'loads the yaml file only once' do + expect(YAML).to receive(:load_file).once.and_return( + 'veteran_full_name' => %w[identity_information full_name], + 'veteran_address' => %w[contact_information address], + 'veteranSocialSecurityNumber' => %w[identity_information ssn], + 'phoneNumber' => %w[contact_information us_phone], + 'emailAddress' => %w[contact_information email] + ) + instance.prefill + end + end end end