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
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
# 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
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
services = [
...
VAProfile::ContactInformation::Configuration.instance.breakers_service,
...
]
vets-api/lib/va_profile/contact_information/person_response.rb
# 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
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
...
# Settings for VAProfile
vet360:
url: "https://int.vet360.va.gov"
contact_information:
cache_enabled: false
enabled: true
timeout: 30
mock: false
...
Help and feedback
Get help from the Platform Support Team in Slack.
Submit a feature idea to the Platform.