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

More cat 4 #2879

Merged
merged 36 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a5d7133
add cat4 aabd_and_ssi validator
jtimpe Mar 8, 2024
1ccebcf
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Mar 8, 2024
86809c3
uncomment logs
jtimpe Mar 8, 2024
b1b11cc
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Mar 8, 2024
7e2ce95
up timeout
jtimpe Mar 8, 2024
5974ee6
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Mar 8, 2024
fe50af7
validate case closure reasons
jtimpe Mar 19, 2024
5ce1889
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 9, 2024
3e2f8cf
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 9, 2024
a34bc11
fix tests
jtimpe Apr 10, 2024
1448761
rm irrelevant test case
jtimpe Apr 10, 2024
efdaa2c
rm comment
jtimpe Apr 10, 2024
05bbb7a
clean up comments
jtimpe Apr 10, 2024
76bde1e
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 12, 2024
89b767b
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 12, 2024
d979685
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 12, 2024
c64a8cb
fix tests
jtimpe Apr 15, 2024
39b471f
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe Apr 19, 2024
881fcec
add 2 as option for territory aabd
jtimpe Apr 19, 2024
680f62e
extrapolate `get_years_apart` to utils
jtimpe Apr 19, 2024
664d0c9
or -> and
jtimpe Apr 19, 2024
7346a3e
fix test
jtimpe Apr 19, 2024
c3b9c33
lint
jtimpe Apr 19, 2024
1269d2c
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe May 1, 2024
b7521f9
Merge branch '2842-cat-4-related-records' into 2842-cat-4-remaining-s…
jtimpe May 13, 2024
82f79eb
add missing program_type to tests
jtimpe May 13, 2024
c1c87d9
update adult age to 19
jtimpe May 15, 2024
9195ca1
Merge branch 'develop' into 2842-cat-4-remaining-s2-validators
jtimpe May 15, 2024
f9728ea
for a -> per in cat4 messaging
jtimpe May 16, 2024
89bb216
update closure reason logic
jtimpe May 16, 2024
f6fb71d
lint
jtimpe May 17, 2024
fc7e751
update validator messaging
jtimpe May 17, 2024
8a17eb3
update states rec_ssi logic
jtimpe May 17, 2024
9ce7ea1
add field name to error messages
jtimpe May 17, 2024
3ad8be0
lint
jtimpe May 17, 2024
5e98971
update ftl error language
jtimpe May 17, 2024
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
177 changes: 175 additions & 2 deletions tdrs-backend/tdpservice/parsers/case_consistency_validator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Class definition for Category Four validator."""

from datetime import datetime
from .models import ParserErrorCategoryChoices
from tdpservice.stts.models import STT
from tdpservice.parsers.schema_defs.utils import get_program_model
import logging

Expand Down Expand Up @@ -43,7 +45,7 @@ def __add_record_to_sorted_object(self, record_schema_pair):
class CaseConsistencyValidator:
"""Caches records of the same case and month to perform category four validation while actively parsing."""

def __init__(self, header, generate_error):
def __init__(self, header, stt_type, generate_error):
self.header = header
self.record_schema_pairs = SortedRecordSchemaPairs()
self.current_case = None
Expand All @@ -56,6 +58,7 @@ def __init__(self, header, generate_error):
self.generated_errors = []
self.total_cases_cached = 0
self.total_cases_validated = 0
self.stt_type = stt_type

def __get_model(self, model_str):
"""Return a model for the current program type/section given the model's string name."""
Expand Down Expand Up @@ -138,10 +141,12 @@ def __validate_section1(self, num_errors):
def __validate_section2(self, num_errors):
"""Perform TANF Section 2 category four validation on all cached records."""
num_errors += self.__validate_s2_records_are_related()
num_errors += self.__validate_t5_aabd_and_ssi()
return num_errors

def __validate_family_affiliation(self, num_errors, t1s, t2s, t3s, error_msg):
"""Validate at least one record in t2s+t3s has FAMILY_AFFILIATION == 1."""
num_errors = 0
passed = False
for record, schema in t2s + t3s:
family_affiliation = getattr(record, 'FAMILY_AFFILIATION')
Expand Down Expand Up @@ -186,6 +191,19 @@ def __validate_s1_records_are_related(self):
t3s = reporting_year_cases.get(t3_model, [])

if len(t1s) > 0:
if len(t1s) > 1: # likely to be captured by "no duplicates" validator

Choose a reason for hiding this comment

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

nit: remove comment

for record, schema in t1s[1:]:
self.__generate_and_add_error(
schema,
record,
field='RPT_MONTH_YEAR',
msg=(
f'There should only be one {t1_model_name} record '
f'for a RPT_MONTH_YEAR and CASE_NUMBER.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f'for a RPT_MONTH_YEAR and CASE_NUMBER.'
f'per RPT_MONTH_YEAR and CASE_NUMBER.'

)
)
num_errors += 1

if len(t2s) == 0 and len(t3s) == 0:
for record, schema in t1s:
self.__generate_and_add_error(
Expand All @@ -203,7 +221,7 @@ def __validate_s1_records_are_related(self):
else:
# loop through all t2s and t3s
# to find record where FAMILY_AFFILIATION == 1
num_errors += self.__validate_family_affiliation(num_errors, t1s, t2s, t3s, (
num_errors += self.__validate_family_affiliation(t1s, t2s, t3s, (
f'Every {t1_model_name} record should have at least one corresponding '
f'{t2_model_name} or {t3_model_name} record with the same RPT_MONTH_YEAR and '
f'CASE_NUMBER, where FAMILY_AFFILIATION==1'
Expand Down Expand Up @@ -238,6 +256,65 @@ def __validate_s1_records_are_related(self):

return num_errors

def __validate_case_closure_employment(self, t4, t5s, error_msg):
"""
Validate case closure.

If case closure reason = 01:employment, then at least one person on
the case must have employment status = 1:Yes in the same month.
"""
num_errors = 0
t4_record, t4_schema = t4

passed = False
for record, schema in t5s:
employment_status = getattr(record, 'EMPLOYMENT_STATUS')

if employment_status == 1:
passed = True
break

if not passed:
self.__generate_and_add_error(
t4_schema,
t4_record,
'EMPLOYMENT_STATUS',
error_msg
)
num_errors += 1

return num_errors

def __validate_case_closure_ftl(self, t4, t5s, error_msg):
"""
Validate case closure.

If closure reason = FTL, then at least one person who is HoH
or spouse of HoH on case must have FTL months >=60.
"""
num_errors = 0
t4_record, t4_schema = t4

passed = False
for record, schema in t5s:
relationship_hoh = getattr(record, 'RELATIONSHIP_HOH')
ftl_months = getattr(record, 'COUNTABLE_MONTH_FED_TIME')

if (relationship_hoh == '01' or relationship_hoh == '02') and int(ftl_months) >= 60:
passed = True
break

if not passed:
self.__generate_and_add_error(
t4_schema,
t4_record,
'COUNTABLE_MONTH_FED_TIME',
error_msg
)
num_errors += 1

return num_errors

def __validate_s2_records_are_related(self):
"""
Validate section 2 records are related.
Expand All @@ -260,6 +337,31 @@ def __validate_s2_records_are_related(self):
t5s = reporting_year_cases.get(t5_model, [])

if len(t4s) > 0:
if len(t4s) > 1:
for record, schema in t4s[1:]:
self.__generate_and_add_error(
schema,
record,
field='RPT_MONTH_YEAR',
msg=(
f'There should only be one {t4_model_name} record '
f'for a RPT_MONTH_YEAR and CASE_NUMBER.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f'for a RPT_MONTH_YEAR and CASE_NUMBER.'
f'per RPT_MONTH_YEAR and CASE_NUMBER.'

)
)
num_errors += 1
else:
t4 = t4s[0]
t4_record, t4_schema = t4
closure_reason = getattr(t4_record, 'CLOSURE_REASON')

if closure_reason == '01':
num_errors += self.__validate_case_closure_employment(t4, t5s, (
'At least one person on the case must have employment status = 1:Yes in the same month.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

@jtimpe suggested language for this one from the DIGIT team:

At least one person on the case must have employment status = 1:Yes in the same RPT_MONTH_YEAR since CLOSURE_REASON = 1:Employment/excess earnings.

))
elif closure_reason == '99' and not is_ssp:
num_errors += self.__validate_case_closure_ftl(t4, t5s, (
'At least one person who is HoH or spouse of HoH on case must have FTL months >=60.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

@jtimpe do we mean closure_reason == '03' and not is_ssp?

Screenshot 2024-05-16 141837 PNG

Copy link
Author

Choose a reason for hiding this comment

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

yep, thank you

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
'At least one person who is HoH or spouse of HoH on case must have FTL months >=60.'
'At least one person who is head-of-household or spouse of head-of-household on case must have countable months toward time limit >= 60 since CLOSURE_REASON = 03: federal 5 year time limit.'

))
if len(t5s) == 0:
for record, schema in t4s:
self.__generate_and_add_error(
Expand Down Expand Up @@ -289,3 +391,74 @@ def __validate_s2_records_are_related(self):
num_errors += 1

return num_errors

def __validate_t5_aabd_and_ssi(self):
print('validate t5')
elipe17 marked this conversation as resolved.
Show resolved Hide resolved
num_errors = 0
is_ssp = self.program_type == 'SSP'

t5_model_name = 'M5' if is_ssp else 'T5'
t5_model = self.__get_model(t5_model_name)

is_state = self.stt_type == STT.EntityType.STATE
is_territory = self.stt_type == STT.EntityType.TERRITORY

for rpt_month_year, reporting_year_cases in self.record_schema_pairs.sorted_cases.items():
t5s = reporting_year_cases.get(t5_model, [])

for record, schema in t5s:
rec_aabd = getattr(record, 'REC_AID_TOTALLY_DISABLED')
rec_ssi = getattr(record, 'REC_SSI')
family_affiliation = getattr(record, 'FAMILY_AFFILIATION')
dob = getattr(record, 'DATE_OF_BIRTH')

rpt_month_year_dd = f'{rpt_month_year}01'
rpt_date = datetime.strptime(rpt_month_year_dd, '%Y%m%d')
dob_date = datetime.strptime(dob, '%Y%m%d')
delta = rpt_date - dob_date
age = delta.days/365.25
is_adult = age >= 18
elipe17 marked this conversation as resolved.
Show resolved Hide resolved

if is_territory and is_adult and rec_aabd != 1:
elipe17 marked this conversation as resolved.
Show resolved Hide resolved
self.__generate_and_add_error(
schema,
record,
field='REC_AID_TOTALLY_DISABLED',
msg=(
f'{t5_model_name} Adults in territories must have a valid value for 19C.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f'{t5_model_name} Adults in territories must have a valid value for 19C.'
f'{t5_model_name} Adults in territories must have a valid value for {field}.'

)
)
num_errors += 1
elif is_state and rec_aabd != 2:
elipe17 marked this conversation as resolved.
Show resolved Hide resolved
self.__generate_and_add_error(
schema,
record,
field='REC_AID_TOTALLY_DISABLED',
msg=(
f'{t5_model_name} People in states shouldn\'t have a value of 1.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f'{t5_model_name} People in states shouldn\'t have a value of 1.'
f'{t5_model_name} People in states should not have a value of 1.'

)
)
num_errors += 1

if is_territory and rec_ssi != 2:
self.__generate_and_add_error(
schema,
record,
field='REC_SSI',
msg=(
f'{t5_model_name} People in territories must have a valid value for 19E.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f'{t5_model_name} People in territories must have a valid value for 19E.'
f'{t5_model_name} People in territories must have value = 2:No for 19E.'

)
)
num_errors += 1
elif is_state and family_affiliation == 1 and rec_ssi != 1:
Copy link

@elipe17 elipe17 Apr 18, 2024

Choose a reason for hiding this comment

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

I might be misunderstanding. Shouldn't rec_ssi != 1 be rec_ssi != 2 according to the ticket: for states if FAMILY_AFFILIATION == 1, must have REC_SSI==2?

Copy link
Author

Choose a reason for hiding this comment

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

for states, the value is required to be 1, so i think throwing an error when rec_ssi != 1 makes sense

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jtimpe the logic here should be as follows:

is_state and family_affiliation == 1:

self.__generate_and_add_error(
schema,
record,
field='REC_SSI',
msg=(
f'{t5_model_name} People in states must have a valid value.'
Copy link
Collaborator

Choose a reason for hiding this comment

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

@jtimpe can we add the following? I just noticed that the other columns in the error report dont make reference to which field or row this error message is associated with. thats probably another ticket, so in the meantime, we should reference field in the error message:

f'{t5_model_name}: People in states must have a valid value for {field}'

Copy link
Author

Choose a reason for hiding this comment

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

per our discussion, i've hard-coded the field names into the string for now, and created #2996 to capture a dynamic approach to including the field/friendly name

)
)
num_errors += 1

return num_errors
7 changes: 6 additions & 1 deletion tdrs-backend/tdpservice/parsers/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ def parse_datafile(datafile, dfs):
bulk_create_errors({1: header_errors}, 1, flush=True)
return errors

case_consistency_validator = CaseConsistencyValidator(header, util.make_generate_parser_error(datafile, None))
# TODO: write a test for this line
case_consistency_validator = CaseConsistencyValidator(
header,
datafile.stt.type,
util.make_generate_parser_error(datafile, None)
)

field_values = schema_defs.header.get_field_values_by_names(header_line,
{"encryption", "tribe_code", "state_fips"})
Expand Down
10 changes: 5 additions & 5 deletions tdrs-backend/tdpservice/parsers/test/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class Meta:
STRATUM = 1
ZIP_CODE = "11111"
DISPOSITION = 1
CLOSURE_REASON = '01'
CLOSURE_REASON = '02'
REC_SUB_HOUSING = 1
REC_MED_ASSIST = 1
REC_FOOD_STAMPS = 1
Expand All @@ -287,7 +287,7 @@ class Meta:
RPT_MONTH_YEAR = 202301
CASE_NUMBER = "1"
FAMILY_AFFILIATION = 1
DATE_OF_BIRTH = "02091997"
DATE_OF_BIRTH = "19970209"
SSN = "123456789"
RACE_HISPANIC = 1
RACE_AMER_INDIAN = 1
Expand Down Expand Up @@ -515,7 +515,7 @@ class Meta:
STRATUM = 1
ZIP_CODE = "11111"
DISPOSITION = 1
CLOSURE_REASON = '01'
CLOSURE_REASON = '02'
REC_SUB_HOUSING = 1
REC_MED_ASSIST = 1
REC_FOOD_STAMPS = 1
Expand All @@ -533,7 +533,7 @@ class Meta:
RPT_MONTH_YEAR = 202301
CASE_NUMBER = "1"
FAMILY_AFFILIATION = 1
DATE_OF_BIRTH = "02091997"
DATE_OF_BIRTH = "19970209"
SSN = "123456789"
RACE_HISPANIC = 1
RACE_AMER_INDIAN = 1
Expand Down Expand Up @@ -773,7 +773,7 @@ class Meta:
STRATUM = 1
ZIP_CODE = "11111"
DISPOSITION = 1
CLOSURE_REASON = '01'
CLOSURE_REASON = '02'
REC_SUB_HOUSING = 1
REC_MED_ASSIST = 1
REC_FOOD_STAMPS = 1
Expand Down
Loading
Loading