From c762d12e12dda1a151087622b04a0f210471434c Mon Sep 17 00:00:00 2001 From: Wayne Weibel Date: Tue, 16 Apr 2024 10:30:38 -0400 Subject: [PATCH] Lighthouse Benefits Intake Metadata (#16333) --- lib/lighthouse/benefits_intake/metadata.rb | 132 +++++++++++++++++ .../benefits_intake/metadata_spec.rb | 139 ++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 lib/lighthouse/benefits_intake/metadata.rb create mode 100644 spec/lib/lighthouse/benefits_intake/metadata_spec.rb diff --git a/lib/lighthouse/benefits_intake/metadata.rb b/lib/lighthouse/benefits_intake/metadata.rb new file mode 100644 index 00000000000..6adec07abe9 --- /dev/null +++ b/lib/lighthouse/benefits_intake/metadata.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +module BenefitsIntake + ## + # Validate the required metadata which must accompany an upload: + # + # { + # 'veteranFirstName': String, + # 'veteranLastName': String, + # 'fileNumber': String, # 8-9 digits + # 'zipCode': String, # 5 or 9 digits + # 'source': String, + # 'docType': String, + # 'businessLine': String, # optional; enum in BUSINESS_LINE + # } + # + # https://developer.va.gov/explore/api/benefits-intake/docs + # + class Metadata + BUSINESS_LINE = { + CMP: 'Compensation requests such as those related to disability, unemployment, and pandemic claims', + PMC: 'Pension requests including survivor’s pension', + INS: 'Insurance such as life insurance, disability insurance, and other health insurance', + EDU: 'Education benefits, programs, and affiliations', + VRE: 'Veteran Readiness & Employment such as employment questionnaires, ' \ + 'employment discrimination, employment verification', + BVA: 'Board of Veteran Appeals', + FID: 'Fiduciary / financial appointee, including family member benefits', + NCA: 'National Cemetery Administration', + OTH: 'Other (this value if used, will be treated as CMP)' + }.freeze + + # rubocop:disable Metrics/ParameterLists + def self.generate(first_name, last_name, file_number, zip_code, source, doc_type, business_line = nil) + validate({ + 'veteranFirstName' => first_name, + 'veteranLastName' => last_name, + 'fileNumber' => file_number, + 'zipCode' => zip_code, + 'source' => source, + 'docType' => doc_type, + 'businessLine' => business_line + }) + end + # rubocop:enable Metrics/ParameterLists + + def self.validate(metadata) + validate_first_name(metadata) + .then { |m| validate_last_name(m) } + .then { |m| validate_file_number(m) } + .then { |m| validate_zip_code(m) } + .then { |m| validate_source(m) } + .then { |m| validate_doc_type(m) } + .then { |m| validate_business_line(m) } + end + + def self.validate_first_name(metadata) + validate_presence_and_stringiness(metadata['veteranFirstName'], 'veteran first name') + + first_name = I18n.transliterate(metadata['veteranFirstName']).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip.first(50) + validate_nonblank(first_name, 'veteran first name') + + metadata['veteranFirstName'] = first_name + metadata + end + + def self.validate_last_name(metadata) + validate_presence_and_stringiness(metadata['veteranLastName'], 'veteran last name') + + last_name = I18n.transliterate(metadata['veteranLastName']).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip.first(50) + validate_nonblank(last_name, 'veteran last name') + + metadata['veteranLastName'] = last_name + metadata + end + + def self.validate_file_number(metadata) + validate_presence_and_stringiness(metadata['fileNumber'], 'file number') + unless metadata['fileNumber'].match?(/^\d{8,9}$/) + raise ArgumentError, 'file number is invalid. It must be 8 or 9 digits' + end + + metadata + end + + def self.validate_zip_code(metadata) + validate_presence_and_stringiness(metadata['zipCode'], 'zip code') + + zip_code = metadata['zipCode'].dup.gsub(/[^0-9]/, '') + zip_code.insert(5, '-') if zip_code.match?(/\A[0-9]{9}\z/) + zip_code = '00000' unless zip_code.match?(/\A[0-9]{5}(-[0-9]{4})?\z/) + + metadata['zipCode'] = zip_code + + metadata + end + + def self.validate_source(metadata) + validate_presence_and_stringiness(metadata['source'], 'source') + + metadata + end + + def self.validate_doc_type(metadata) + validate_presence_and_stringiness(metadata['docType'], 'doc type') + + metadata + end + + def self.validate_business_line(metadata) + bl = metadata['businessLine'] + if bl + bl = bl.dup.to_s.upcase.to_sym + bl = :OTH unless BUSINESS_LINE.key?(bl) + metadata['businessLine'] = bl.to_s + else + metadata.delete('businessLine') + end + + metadata + 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 + + def self.validate_nonblank(value, error_label) + raise ArgumentError, "#{error_label} is blank" if value.blank? + end + end +end diff --git a/spec/lib/lighthouse/benefits_intake/metadata_spec.rb b/spec/lib/lighthouse/benefits_intake/metadata_spec.rb new file mode 100644 index 00000000000..70e7a79821a --- /dev/null +++ b/spec/lib/lighthouse/benefits_intake/metadata_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'lighthouse/benefits_intake/metadata' + +RSpec.describe BenefitsIntake::Metadata do + let(:meta) { described_class } + + context 'with valid parameters' do + let(:valid) do + { + 'veteranFirstName' => 'firstname', + 'veteranLastName' => 'lastname', + 'fileNumber' => '123456789', + 'zipCode' => '12345-5555', + 'source' => 'source', + 'docType' => 'doc_type', + 'businessLine' => 'BVA' + } + end + + it 'returns unmodified metadata' do + data = meta.generate('firstname', 'lastname', '123456789', '12345-5555', 'source', 'doc_type', 'BVA') + expect(data).to eq(valid) + end + + it 'returns corrected metadata' do + data = meta.generate('first_name', 'last_name', '123456789', '123455555', 'source', 'doc_type', :bva) + expect(data).to eq(valid) + end + end + + context 'malformed data' do + it 'truncates names' do + charset = Array('a'..'z') + Array('A'..'Z') + ['-', ' ', '/'] + firstname = Array.new(rand(50..100)) { charset.sample }.join + lastname = Array.new(rand(50..100)) { charset.sample }.join + + first50 = meta.validate_first_name({ 'veteranFirstName' => firstname }) + expect(first50).to eq({ 'veteranFirstName' => firstname.strip[0..49] }) + + last50 = meta.validate_last_name({ 'veteranLastName' => lastname }) + expect(last50).to eq({ 'veteranLastName' => lastname.strip[0..49] }) + end + + it 'errors on substituted blank names' do + expect do + meta.validate_first_name({ 'veteranFirstName' => '23&_$!42' }) + end.to raise_error(ArgumentError, 'veteran first name is blank') + + expect do + meta.validate_last_name({ 'veteranLastName' => '23&_$!42' }) + end.to raise_error(ArgumentError, 'veteran last name is blank') + end + + it 'corrects malformed zipcode' do + zip = meta.validate_zip_code({ 'zipCode' => '12345TEST' }) + expect(zip).to eq({ 'zipCode' => '12345' }) + + zip = meta.validate_zip_code({ 'zipCode' => '12345TEST6789' }) + expect(zip).to eq({ 'zipCode' => '12345-6789' }) + + zip = meta.validate_zip_code({ 'zipCode' => '123456789123456789' }) + expect(zip).to eq({ 'zipCode' => '00000' }) + end + + it 'corrects malformed business_line' do + zip = meta.validate_business_line({ 'businessLine' => :BVA }) + expect(zip).to eq({ 'businessLine' => 'BVA' }) + + zip = meta.validate_business_line({ 'businessLine' => :pmc }) + expect(zip).to eq({ 'businessLine' => 'PMC' }) + + zip = meta.validate_business_line({ 'businessLine' => 'pmc' }) + expect(zip).to eq({ 'businessLine' => 'PMC' }) + + zip = meta.validate_business_line({ 'businessLine' => :TEST }) + expect(zip).to eq({ 'businessLine' => 'OTH' }) + + zip = meta.validate_business_line({ 'businessLine' => 'TEST' }) + expect(zip).to eq({ 'businessLine' => 'OTH' }) + + zip = meta.validate_business_line({ 'businessLine' => nil }) + expect(zip).to eq({}) + end + + it 'errors on invalid file number' do + expect do + meta.validate_file_number({ 'fileNumber' => '123TEST89' }) + end.to raise_error(ArgumentError, 'file number is invalid. It must be 8 or 9 digits') + + expect do + meta.validate_file_number({ 'fileNumber' => '123456789123456789' }) + end.to raise_error(ArgumentError, 'file number is invalid. It must be 8 or 9 digits') + + expect do + meta.validate_file_number({ 'fileNumber' => '12345' }) + end.to raise_error(ArgumentError, 'file number is invalid. It must be 8 or 9 digits') + end + end + + describe '#validate_presence_and_stringiness' do + it 'raises a missing exception' do + expect do + meta.validate_presence_and_stringiness(nil, 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is missing') + + expect do + meta.validate_presence_and_stringiness(false, 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is missing') + end + + it 'raises a non-string exception' do + expect do + meta.validate_presence_and_stringiness(12, 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is not a string') + + expect do + meta.validate_presence_and_stringiness(true, 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is not a string') + + expect do + meta.validate_presence_and_stringiness({}, 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is not a string') + end + + it 'raises a blank exception' do + expect do + meta.validate_nonblank('', 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is blank') + + expect do + meta.validate_nonblank(' ', 'TEST FIELD') + end.to raise_error(ArgumentError, 'TEST FIELD is blank') + end + end + + # end Rspec.describe +end