Skip to content

Commit

Permalink
Add flash picker for ALS with feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
dfitchett committed Nov 15, 2024
1 parent cfb155c commit 049207f
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 3 deletions.
3 changes: 2 additions & 1 deletion app/models/saved_claim/disability_compensation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def to_submission_data(user)
form4142 = EVSS::DisabilityCompensationForm::Form4142.new(user, @form_hash.deep_dup).translate
form526 = @form_hash.deep_dup
dis_form = EVSS::DisabilityCompensationForm::DataTranslationAllClaim.new(user, form526, form4142.present?).translate
claimed_disabilities = dis_form.dig('form526', 'disabilities')
form526_uploads = form526['form526'].delete('attachments')

{
Expand All @@ -33,7 +34,7 @@ def to_submission_data(user)
@form_hash.deep_dup).translate,
Form526Submission::FORM_8940 => EVSS::DisabilityCompensationForm::Form8940.new(user,
@form_hash.deep_dup).translate,
'flashes' => BGS::DisabilityCompensationFormFlashes.new(user, @form_hash.deep_dup).translate
'flashes' => BGS::DisabilityCompensationFormFlashes.new(user, @form_hash.deep_dup, claimed_disabilities).translate
}.to_json
end
end
79 changes: 79 additions & 0 deletions app/services/claim_fast_tracking/flash_picker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

module ClaimFastTracking
class FlashPicker
DEFAULT_FUZZY_TOLERANCE = 0.2
MIN_FUZZY_MATCH_LENGTH = 6
MIN_LENGTH_RATIO = 0.9

ALS_DC = 8017
ALS_PARTIAL_MATCH_TERMS = [
'amyotrophic lateral sclerosis',
'(als)'
].freeze
ALS_MATCH_TERMS = (ALS_PARTIAL_MATCH_TERMS + [
'als',
'lou gehrig disease',
'lou gehrigs disease',
'lou gehrig\'s disease',
'lou gehrig',
'lou gehrigs',
'lou gehrig\'s'
]).freeze

def self.als?(claimed_disabilities)
return if claimed_disabilities.pluck('diagnosticCode').include?(ALS_DC)

claimed_disabilities.map { |disability| disability['name']&.downcase }.compact.any? do |name|
partial_matches?(name, ALS_PARTIAL_MATCH_TERMS) || matches?(name, ALS_MATCH_TERMS)
end
end

def self.partial_matches?(name, match_terms)
match_terms = [match_terms] unless match_terms.is_a?(Array)

match_terms.any? { |term| name.include?(term) }
end

def self.matches?(name,
match_terms,
tolerance = DEFAULT_FUZZY_TOLERANCE,
min_length_ratio = MIN_LENGTH_RATIO,
min_length_limit = MIN_FUZZY_MATCH_LENGTH)
match_terms = [match_terms] unless match_terms.is_a?(Array)

match_terms.any? do |term|
# Early exact match check (case insensitive)
return true if name.casecmp?(term)

# Prevent fuzzy matching for very short terms (e.g., less than min_length_limit)
next false if name.length < min_length_limit || term.length < min_length_limit

# Calculate the length ratio based on the shorter and longer lengths
shorter_length = [name.length, term.length].min
longer_length = [name.length, term.length].max

# Skip comparison if the length ratio is below minimum length ratio, indicating a significant length difference
next false if shorter_length.to_f / longer_length < min_length_ratio

# Calculate the Levenshtein threshold based on tolerance and maximum length
return true if fuzzy_match?(name, term, longer_length, tolerance)
end
end

def self.fuzzy_match?(name, term, longer_length, tolerance = DEFAULT_FUZZY_TOLERANCE)
threshold = (longer_length * tolerance).ceil
distance = StringHelpers.levenshtein_distance(name, term)

if distance - 1 == threshold
Rails.logger.info(
'FlashPicker close fuzzy match for condition',
{ name: name, match_term: term, distance: distance, threshold: threshold }
)
end
distance <= threshold
end

private_class_method :partial_matches?, :matches?, :fuzzy_match?
end
end
4 changes: 4 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ features:
disability_526_ep_merge_api:
actor_type: user
description: enables sending 526 claims with a pending EP to VRO EP Merge API for automated merging.
disability_526_ee_process_als_flash:
actor_type: user
description: enables adding applicable flashes to disability_526 prior to submission.
enable_in_development: true
disability_526_toxic_exposure:
actor_type: user
description: enables new pages, processing, and submission of toxic exposure claims
Expand Down
14 changes: 13 additions & 1 deletion lib/bgs/disability_compensation_form_flashes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

module BGS
class DisabilityCompensationFormFlashes
def initialize(user, form_content)
def initialize(user, form_content, claimed_disabilities)
@user = user
@form_content = form_content['form526']
@claimed_disabilities = claimed_disabilities
@flashes = []
end

Expand Down Expand Up @@ -32,6 +33,7 @@ def translate
@flashes << 'Terminally Ill' if terminally_ill?
@flashes << 'Priority Processing - Veteran over age 85' if over_85?
@flashes << 'POW' if pow?
@flashes << 'Amyotrophic Lateral Sclerosis' if als?
@flashes
end

Expand All @@ -50,5 +52,15 @@ def over_85?
def pow?
@form_content['confinements'].present?
end

def als?
feature_enabled = Flipper.enabled?(:disability_526_ee_process_als_flash, @user)
add_als = ClaimFastTracking::FlashPicker.als?(@claimed_disabilities)
Rails.logger.error('FlashPicker for ALS', { feature_enabled:, add_als: })
feature_enabled && add_als
rescue => e
Rails.logger.error("Failed to determine need for ALS flash: #{e.message}.", backtrace: e.backtrace)
false
end
end
end
49 changes: 48 additions & 1 deletion spec/lib/bgs/disability_compensation_form_flashes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require 'bgs/disability_compensation_form_flashes'

Rspec.describe BGS::DisabilityCompensationFormFlashes do
subject { described_class.new(user, form_content) }
subject { described_class.new(user, form_content, disabilities) }

let(:form_content) do
JSON.parse(
Expand All @@ -13,6 +13,21 @@
end

let(:flashes) { ['Homeless', 'Priority Processing - Veteran over age 85', 'POW'] }
let(:disabilities) do
[
{
'name' => 'PTSD (post traumatic stress disorder)',
'diagnosticCode' => 9999,
'disabilityActionType' => 'NEW',
'ratedDisabilityId' => '1100583'
},
{
'name' => 'PTSD personal trauma',
'disabilityActionType' => 'SECONDARY',
'serviceRelevance' => "Caused by a service-connected disability\nPTSD (post traumatic stress disorder)"
}
]
end
let(:user) { build(:disabilities_compensation_user) }

before do
Expand All @@ -23,5 +38,37 @@
it 'returns correctly flashes to send to async job' do
expect(subject.translate).to eq flashes
end

context 'when the user has ALS condition' do
let(:disabilities) do
[
{
'name' => 'ALS (amyotrophic lateral sclerosis)',
'disabilityActionType' => 'NEW',
'serviceRelevance' => "Caused by an in-service event, injury, or exposure\ntest"
}
]
end

context 'when feature is enabled' do
before do
allow(Flipper).to receive(:enabled?).with(:disability_526_ee_process_als_flash, user).and_return(true)
end

it 'returns ALS flash' do
expect(subject.translate).to include('Amyotrophic Lateral Sclerosis')
end
end

context 'when feature is disabled' do
before do
allow(Flipper).to receive(:enabled?).with(:disability_526_ee_process_als_flash, user).and_return(false)
end

it 'returns without flash' do
expect(subject.translate).not_to include('Amyotrophic Lateral Sclerosis')
end
end
end
end
end
98 changes: 98 additions & 0 deletions spec/services/claim_fast_tracking/flash_picker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ClaimFastTracking::FlashPicker do
subject { described_class }

describe '#als?' do
context 'when testing for ALS' do
context 'when the disabilities is empty' do
let(:disabilities) { [] }

it 'returns an empty array' do
expect(subject.als?(disabilities)).to eq(false)
end
end

context 'when the disabilities does not contain ALS' do
let(:disabilities) { [{ 'name' => 'Tinnitus', 'diagnosticCode' => 6260 }] }

it 'returns false' do
expect(subject.als?(disabilities)).to eq(false)
end
end

context 'when the disability name exactly matches any of the ALS_TERMS' do
described_class::ALS_MATCH_TERMS.each do |term|
it "returns true for term #{term}" do
expect(subject.als?([{ 'name' => term }])).to eq(true)
end
end
end

context 'when the disability name has partial match' do
[
{ condition: 'amyotrophic lateral sclerosis', reason: 'full name' },
{ condition: '(als)', reason: 'acronym only in parentheses' },
{
condition: 'amyotrophic lateral sclerosis with lower extremity weakness, abnormal speech and abnormal gait',
reason: 'full name with symptoms'
},
{ condition: 'als amyotrophic lateral sclerosis', reason: 'full name with acronym on left' },
{ condition: 'als (amyotrophic lateral sclerosis)', reason: 'full name in parentheses with acronym on left' },
{ condition: 'als amyotrophic lateral sclerosis', reason: 'full name with acronym on left' },
{ condition: '(als) amyotrophic lateral sclerosis', reason: 'full name with acronym on left in parentheses' },
{ condition: 'amyotrophic lateral sclerosis als', reason: 'full name with acronym on right' },
{ condition: '(amyotrophic lateral sclerosis) als',
reason: 'full name in parentheses with acronym on right' },
{ condition: 'amyotrophic lateral sclerosis (als)',
reason: 'full name with acronym on right in parentheses' },
{ condition: 'amyotropic lateril scerolses (als)',
reason: 'full name with several letter typo and acronym in parentheses' }
].each do |test_case|
it "returns true for term #{test_case[:reason]}" do
disabilities = [{ 'name' => test_case[:condition] }]
expect(subject.als?(disabilities)).to eq(true)
end
end
end

context 'when the disabilities contains a fuzzy match' do
[
{ condition: 'amyotrophic lateral scleroses', reason: 'Pluralization error' },
{ condition: 'Amyothrophic lateral sclerosis', reason: 'Minor typo' },
{ condition: 'amyotrophic lateral sclerosiss', reason: 'Minor double letter typo' },
{ condition: 'amyotropic lareral sclerosiss', reason: 'several letter typo' },
{ condition: 'amyotrophic lateral scelrosis', reason: 'Phonetic misspelling' },
{ condition: 'lou gherig disease', reason: 'Phonetic misspelling of Gehrig' },
{ condition: 'lou gehrigs desease', reason: 'Double typo' },
{ condition: 'lou gehrigs desase', reason: 'Phonetic error' },
{ condition: "lou gehrig's disease", reason: 'Included Apostrophe with disease' },
{ condition: "lou gehrig'", reason: 'Included Apostrophe without s' },
{ condition: 'lou gehrig', reason: 'Missing possessive "s"' }
].each do |test_case|
it "returns true for term with #{test_case[:reason]}" do
disabilities = [{ 'name' => test_case[:condition] }]
expect(subject.als?(disabilities)).to eq(true)
end
end
end

context 'when the disabilities does not contains any fuzzy match' do
[
{ condition: 'ALT', reason: 'wrong acronym but too small to fuzzy match' },
{ condition: 'sclerosis disease', reason: 'Too vague' },
{ condition: 'Lou diseases', reason: 'Doesn’t specify' },
{ condition: 'lateral disease', reason: 'Partial match with missing context' },
{ condition: 'neuro disease', reason: 'Different condition entirely' }
].each do |test_case|
it "returns false for term with #{test_case[:reason]}" do
disabilities = [{ 'name' => test_case[:condition] }]
expect(subject.als?(disabilities)).to eq(false)
end
end
end
end
end
end

0 comments on commit 049207f

Please sign in to comment.