Skip to main content
Skip table of contents

Service Object Implementation

Introduction

Creating a service backend endpoint for http://va.gov is a common task for developers. Due to the complexity of integrating with legacy VA REST or SOAP applications we've developed a pattern for making these connections. This document offers sample implementations of the service class pattern within the Vets API codebase.

vets-api/lib/va_profile/contact_information/service.rb

CODE
require 'common/client/concerns/monitoring'
require 'common/client/errors'
require 'va_profile/service'
require 'va_profile/stats'
require_relative 'configuration'
require_relative 'transaction_response'

module VAProfile
  module ContactInformation
    class Service < VAProfile::Service
      CONTACT_INFO_CHANGE_TEMPLATE = Settings.vanotify.services.va_gov.template_id.contact_info_change
      EMAIL_PERSONALISATIONS = {
        address: 'Address',
        residence_address: 'Home address',
        correspondence_address: 'Mailing address',
        email: 'Email address',
        phone: 'Phone number',
        home_phone: 'Home phone number',
        mobile_phone: 'Mobile phone number',
        work_phone: 'Work phone number'
      }.freeze

      include Common::Client::Concerns::Monitoring

      configuration VAProfile::ContactInformation::Configuration
 
      ...
      def update_email(email)
        update_model(email, 'email', 'email')
      end
      
      ...
      def post_email(email)
        post_or_put_data(:post, email, 'emails', EmailTransactionResponse)
      end

      # PUTs an updated address to the VAProfile API
      # @param email [VAProfile::Models::Email] the email to update
      # @return [VAProfile::ContactInformation::EmailTransactionResponse] response wrapper around a transaction object
      def put_email(email)
        old_email =
          begin
            @user.va_profile_email
          rescue
            nil
          end

        response = post_or_put_data(:put, email, 'emails', EmailTransactionResponse)

        transaction = response.transaction
        OldEmail.create(transaction_id: transaction.id, email: old_email) if transaction.received? && old_email.present?

        response
      end

      # GET's the status of an email transaction from the VAProfile api
      # @param transaction_id [int] the transaction_id to check
      # @return [VAProfile::ContactInformation::EmailTransactionResponse] response wrapper around a transaction object
      def get_email_transaction_status(transaction_id)
        route = "#{@user.vet360_id}/emails/status/#{transaction_id}"
        transaction_status = get_transaction_status(route, EmailTransactionResponse)

        send_email_change_notification(transaction_status)

        transaction_status
      end
      
      ...
      def get_email_personalisation(type)
        { 'contact_info' => EMAIL_PERSONALISATIONS[type] }
      end

      def send_contact_change_notification(transaction_status, personalisation)
        return unless Flipper.enabled?(:contact_info_change_email, @user)

        transaction = transaction_status.transaction

        if transaction.completed_success?
          transaction_id = transaction.id
          return if TransactionNotification.find(transaction_id).present?

          email = @user.va_profile_email
          return if email.blank?

          VANotifyEmailJob.perform_async(
            email,
            CONTACT_INFO_CHANGE_TEMPLATE,
            get_email_personalisation(personalisation)
          )

          TransactionNotification.create(transaction_id:)
        end
      end

      def send_email_change_notification(transaction_status)
        return unless Flipper.enabled?(:contact_info_change_email, @user)

        transaction = transaction_status.transaction

        if transaction.completed_success?
          old_email = OldEmail.find(transaction.id)
          return if old_email.nil?

          personalisation = get_email_personalisation(:email)

          VANotifyEmailJob.perform_async(old_email.email, CONTACT_INFO_CHANGE_TEMPLATE, personalisation)
          if transaction_status.new_email.present?
            VANotifyEmailJob.perform_async(
              transaction_status.new_email,
              CONTACT_INFO_CHANGE_TEMPLATE,
              personalisation
            )
          end

          old_email.destroy
        end
      end

vets-api/lib/va_profile/contact_information/configuration.rb

CODE
# frozen_string_literal: true

require 'va_profile/configuration'

module VAProfile
  module ContactInformation
    class Configuration < VAProfile::Configuration
      self.read_timeout = VAProfile::Configuration::SETTINGS.contact_information.timeout || 30

      def base_path
        "#{VAProfile::Configuration::SETTINGS.url}/contact-information-hub/cuf/contact-information/v1"
      end

      def service_name
        'VAProfile/ContactInformation'
      end

      def mock_enabled?
        VAProfile::Configuration::SETTINGS.contact_information.mock || false
      end
    end
  end
end

vets-api/lib/va_profile/configuration.rb

CODE
require 'common/client/configuration/rest'
require_relative 'models/base'

module VAProfile
  class Configuration < Common::Client::Configuration::REST
    SETTINGS = Settings.va_profile || Settings.vet360

    def self.base_request_headers
      super.merge('cufSystemName' => VAProfile::Models::Base::SOURCE_SYSTEM)
    end

    def connection
      @conn ||= Faraday.new(base_path, headers: base_request_headers, request: request_options) do |faraday|
        faraday.use      :breakers
        faraday.use      Faraday::Response::RaiseError

        faraday.response :snakecase, symbolize: false
        faraday.response :json, content_type: /\bjson/ # ensures only json content types parsed
        faraday.response :betamocks if mock_enabled?
        faraday.adapter Faraday.default_adapter
      end
    end

    def mock_enabled?
      false
    end
  end
end

config/initializers/breakers.rb

CODE
services = [
...
  VAProfile::ContactInformation::Configuration.instance.breakers_service,
...
 
]

vets-api/lib/va_profile/contact_information/person_response.rb

CODE
# frozen_string_literal: true

require 'va_profile/response'
require 'va_profile/models/person'

module VAProfile
  module ContactInformation
    class PersonResponse < VAProfile::Response
      attribute :person, VAProfile::Models::Person

      attr_reader :response_body

      def self.from(raw_response = nil)
        @response_body = raw_response&.body

        new(
          raw_response&.status,
          person: VAProfile::Models::Person.build_from(@response_body&.dig('bio'))
        )
      end

      def cache?
        super || (status >= 400 && status < 500)
      end
    end
  end
end

vets-api/lib/va_profile/contact_information/transaction_response.rb

CODE
require 'va_profile/models/transaction'
require 'va_profile/response'

module VAProfile
  module ContactInformation
    class TransactionResponse < VAProfile::Response
      extend SentryLogging

      attribute :transaction, VAProfile::Models::Transaction
      ERROR_STATUS = 'COMPLETED_FAILURE'

      attr_reader :response_body

      def self.from(raw_response = nil)
        @response_body = raw_response&.body

        if error?
          log_message_to_sentry(
            'VAProfile transaction error',
            :error,
            { response_body: @response_body },
            error: :va_profile
          )
        end

        new(
          raw_response&.status,
          transaction: VAProfile::Models::Transaction.build_from(@response_body)
        )
      end

      def self.error?
        @response_body.try(:[], 'tx_status') == ERROR_STATUS
      end
    end
    
    ...
    class EmailTransactionResponse < TransactionResponse
      attribute :response_body, String

      def self.from(*args)
        return_val = super

        return_val.response_body = @response_body

        return_val
      end

      def new_email
        tx_output = response_body['tx_output'][0]
        return if tx_output['effective_end_date'].present?

        tx_output['email_address_text']
      end
    end
    
    ...
  end
end

vets-api/config/settings.yml

CODE
...
# Settings for VAProfile
vet360:
  url: "https://int.vet360.va.gov"
  contact_information:
    cache_enabled: false
    enabled: true
    timeout: 30
    mock: false
...

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.