Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

103453 - Add validation for VES data - IVC CHAMPVA #21138

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions modules/ivc_champva/app/services/ivc_champva/ves_data_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

# TODO: add validators for non-required, but structure constrained types:
# - validate phone number structure: ^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$
# - validate gender values
# - validate relationship to veteran
Comment on lines +3 to +6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside the scope of this ticket, but there will be a followup


module IvcChampva
class VesDataValidator
# This function will run through all the individual validators
def self.validate(request_body)
validate_application_type(request_body)
.then { |rb| validate_application_uuid(rb) }
.then { |rb| validate_sponsor(rb) }
.then { |rb| validate_beneficiaries(rb) }
.then { |rb| validate_certification(rb) }
end

def self.validate_sponsor(request_body)
sponsor = request_body[:sponsor]
validate_first_name(sponsor)
.then { |s| validate_last_name(s) }
.then { |s| validate_sponsor_address(s) }
.then { |s| validate_date_of_birth(s) }
.then { |s| validate_person_uuid(s) }
.then { |s| validate_ssn(s) }

request_body
end

def self.validate_beneficiaries(request_body)
beneficiaries = request_body[:beneficiaries]
raise ArgumentError, 'beneficiaries is invalid. Must be an array' unless beneficiaries.is_a?(Array)

beneficiaries.each do |beneficiary|
validate_beneficiary(beneficiary)
end

request_body
end

def self.validate_beneficiary(beneficiary)
validate_first_name(beneficiary)
.then { |b| validate_last_name(b) }
.then { |b| validate_date_of_birth(b) }
.then { |b| validate_person_uuid(b) }
.then { |b| validate_beneficiary_address(b) }
.then { |b| validate_ssn(b) }

beneficiary
end

def self.validate_certification(request_body)
certification = request_body[:certification]

validate_presence_and_stringiness(certification[:signature], 'certification signature')
validate_date(certification[:signatureDate], 'certification signature date')

request_body
end

# ------------------------------------------------------------ #
# ------------------------------------------------------------ #
Comment on lines +62 to +63
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All methods below this line are the individual validation components we roll together up above.

There are currently some tests, but the goal is to get much wider coverage of these constituent parts in a follow-on PR.


def self.validate_application_type(request_body)
validate_presence_and_stringiness(request_body[:applicationType], 'application type')

request_body
end

def self.validate_application_uuid(request_body)
validate_presence_and_stringiness(request_body[:applicationUUID], 'application UUID')
validate_uuid_length(request_body[:applicationUUID], 'application UUID')

request_body
end

def self.validate_first_name(object)
validate_presence_and_stringiness(object[:firstName], 'first name')
object[:firstName] = transliterate_and_strip(object[:firstName])

object
end

def self.validate_last_name(object)
validate_presence_and_stringiness(object[:lastName], 'last name')
object[:lastName] = transliterate_and_strip(object[:lastName])

object
end

def self.transliterate_and_strip(text)
# Convert any special UTF-8 chars to nearest ASCII equivalents, drop whitespace
I18n.transliterate(text).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip
end

def self.validate_person_uuid(object)
validate_presence_and_stringiness(object[:personUUID], 'person uuid')
validate_uuid_length(object[:personUUID], 'person uuid')

object
end

def self.validate_date_of_birth(object)
validate_date(object[:dateOfBirth], 'date of birth')

object
end

def self.validate_beneficiary_address(beneficiary)
validate_address(beneficiary[:address], 'beneficiary')

beneficiary
end

def self.validate_sponsor_address(request_body)
validate_address(request_body[:address], 'sponsor')

request_body
end

def self.validate_address(address, name)
validate_presence_and_stringiness(address[:city], "#{name} city")
validate_presence_and_stringiness(address[:state], "#{name} state")
validate_presence_and_stringiness(address[:zipCode], "#{name} zip code")
validate_presence_and_stringiness(address[:streetAddress], "#{name} street address")
end

def self.validate_ssn(request_body)
# TODO: strip out hyphens here? Or do that further up the chain?
validate_presence_and_stringiness(request_body[:ssn], 'ssn')
unless request_body[:ssn].match?(/^(?!(000|666|9))\d{3}(?!00)\d{2}(?!0000)\d{4}$/)
raise ArgumentError, 'ssn is invalid. Must be 9 digits (see regex for more detail)'
end

request_body
end

def self.validate_date(date, name)
validate_presence_and_stringiness(date, name)
raise ArgumentError, 'date is invalid. Must match YYYY-MM-DD' unless date.match?(/^\d{4}-\d{2}-\d{2}$/)

# TODO: once we know the exact date format VES is expecting we can
# do further checks here.
date
end

def self.validate_uuid_length(uuid, name)
# TODO: we may want a more sophisticated validation for uuids
raise ArgumentError, "#{name} is invalid. Must be 36 characters" unless uuid.length == 36
end

def self.validate_presence_and_stringiness(value, error_label)
raise ArgumentError, "#{error_label} is missing" unless value
raise ArgumentError, "#{error_label} is not a string" if value.class != String
end
end
end
131 changes: 131 additions & 0 deletions modules/ivc_champva/spec/services/ves_data_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# frozen_string_literal: true

require 'rails_helper'

valid_data = {
applicationType: 'CHAMPVA Application',
applicationUUID: '12345678-1234-5678-1234-567812345678',
sponsor: {
personUUID: '52345678-1234-5678-1234-567812345678',
firstName: 'Joe',
lastName: 'Johnson',
middleInitial: 'X',
ssn: '123123123',
vaFileNumber: '',
dateOfBirth: '1999-01-01',
dateOfMarriage: '',
isDeceased: true,
dateOfDeath: '1999-01-01',
isDeathOnActiveService: false,
address: {
streetAddress: '123 Certifier Street ',
city: 'Citytown',
state: 'AL',
zipCode: '12312'
}
},
beneficiaries: [
{
personUUID: '62345678-1234-5678-1234-567812345678',
firstName: 'Johnny',
lastName: 'Alvin',
middleInitial: 'T',
ssn: '345345345',
emailAddress: '[email protected]',
phoneNumber: '+1 (555) 555-1234',
gender: 'MALE',
enrolledInMedicare: true,
hasOtherInsurance: true,
relationshipToSponsor: 'CHILD',
childtype: 'ADOPTED',
address: {
streetAddress: '456 Circle Street ',
city: 'Clinton',
state: 'AS',
zipCode: '56790'
},
dateOfBirth: '2000-01-01'
}
],
certification: {
signature: 'certifier jones',
signatureDate: '1999-01-01',
firstName: 'Certifier',
lastName: 'Jones',
middleInitial: 'X',
phoneNumber: '(123) 123-1234'
}
}

describe IvcChampva::VesDataValidator do
describe 'data is valid' do
it 'returns unmodified data' do
# Deep copy our properly formatted data
request_body = Marshal.load(Marshal.dump(valid_data))

validated_data = IvcChampva::VesDataValidator.validate(request_body)

expect(validated_data).to eq(request_body)
end
end

describe 'request_body key has a missing value' do
it 'raises a missing exception' do
expect do
IvcChampva::VesDataValidator.validate_presence_and_stringiness(nil, 'sponsor first name')
end.to raise_error(ArgumentError, 'sponsor first name is missing')
end
end

describe 'string key has a non-string value' do
it 'raises a non-string exception' do
expect do
IvcChampva::VesDataValidator.validate_presence_and_stringiness(12, 'sponsor first name')
end.to raise_error(ArgumentError, 'sponsor first name is not a string')
end
end

describe 'sponsor first name is malformed' do
describe 'contains disallowed characters' do
it 'returns data with disallowed characters of sponsor first name stripped or corrected' do
# Deep copy our valid data
request_body = Marshal.load(Marshal.dump(valid_data))
request_body[:sponsor][:firstName] = '2Jöhn~! - Jo/hn?\\'
expected_sponsor_name = 'John - Jo/hn'

validated_data = IvcChampva::VesDataValidator.validate(request_body)

expect(validated_data[:sponsor][:firstName]).to eq expected_sponsor_name
end
end
end

describe 'sponsor last name is malformed' do
describe 'contains disallowed characters' do
it 'returns data with disallowed characters of sponsor last name stripped or corrected' do
# Deep copy our valid data
request_body = Marshal.load(Marshal.dump(valid_data))
request_body[:sponsor][:lastName] = '2Jöhnşon~!\\'
expected_sponsor_name = 'Johnson'

validated_data = IvcChampva::VesDataValidator.validate(request_body)

expect(validated_data[:sponsor][:lastName]).to eq expected_sponsor_name
end
end
end

describe 'social security number is malformed' do
describe 'too long' do
it 'raises an exception' do
# Deep copy our valid data
request_body = Marshal.load(Marshal.dump(valid_data))
request_body[:sponsor][:ssn] = '1234567890'

expect do
IvcChampva::VesDataValidator.validate(request_body)
end.to raise_error(ArgumentError, 'ssn is invalid. Must be 9 digits (see regex for more detail)')
end
end
end
end
Loading