diff --git a/backend/audit/models/models.py b/backend/audit/models/models.py index b5cfc596f..e35b1d06e 100644 --- a/backend/audit/models/models.py +++ b/backend/audit/models/models.py @@ -249,8 +249,14 @@ def assign_cog_over(self): self.submission_status, ) + audit_date = self.general_information["auditee_fiscal_period_end"] + audit_year = int(audit_date.split("-")[0]) cognizant_agency, oversight_agency = compute_cog_over( - self.federal_awards, self.submission_status, self.ein, self.auditee_uei + self.federal_awards, + self.submission_status, + self.ein, + self.auditee_uei, + audit_year, ) if oversight_agency: self.oversight_agency = oversight_agency diff --git a/backend/support/cog_over.py b/backend/support/cog_over.py index fd268b782..52c04dbfe 100644 --- a/backend/support/cog_over.py +++ b/backend/support/cog_over.py @@ -1,5 +1,6 @@ from collections import defaultdict import logging +import math from django.db.models.functions import Cast from django.db.models import BigIntegerField, Q @@ -13,8 +14,15 @@ COG_LIMIT = 50_000_000 DA_THRESHOLD_FACTOR = 0.25 +FIRST_BASELINE_YEAR = 2019 +BASELINE_VALID_FOR_YEARS = 5 -def compute_cog_over(federal_awards, submission_status, auditee_ein, auditee_uei): +DBKEY_TO_UEI_TRANSITION_YEAR = "2022" + + +def compute_cog_over( + federal_awards, submission_status, auditee_ein, auditee_uei, audit_year +): """ Compute cog or oversight agency for the sac. Return tuple (cog_agency, oversight_agency) @@ -45,13 +53,28 @@ def compute_cog_over(federal_awards, submission_status, auditee_ein, auditee_uei oversight_agency = agency # logger.warning("Assigning an oversight agenct", oversight_agency) return (cognizant_agency, oversight_agency) - cognizant_agency = determine_hist_agency(auditee_ein, auditee_uei) + base_year = calc_base_year(audit_year) + cognizant_agency = determine_hist_agency(auditee_ein, auditee_uei, base_year) if cognizant_agency: return (cognizant_agency, oversight_agency) cognizant_agency = agency return (cognizant_agency, oversight_agency) +def calc_base_year(audit_year): + # Note: 2019 is the first supported baseline year in GSAFAC + # For audit years 2019 through 2023, baseline year is 2019 + # For audit years 2024 through 2028, baseline year is 2024 + # For audit years 2029 through 2033, baseline year is 2029 + # For audit years 2034 through 2038, baseline year is 2034 + # and so on + base_year = ( + math.floor((int(audit_year) - FIRST_BASELINE_YEAR) / BASELINE_VALID_FOR_YEARS) + * BASELINE_VALID_FOR_YEARS + ) + FIRST_BASELINE_YEAR + return str(base_year) + + def calc_award_amounts(awards): total_amount_agency = defaultdict(lambda: 0) total_da_amount_agency = defaultdict(lambda: 0) @@ -77,18 +100,22 @@ def determine_agency(total_amount_expended, max_total_agency, max_da_agency): return agency -def determine_hist_agency(ein, uei): - dbkey = get_dbkey(ein, uei) +def determine_hist_agency(ein, uei, base_year): + dbkey = None + if int(base_year) == FIRST_BASELINE_YEAR: + dbkey = get_dbkey(ein, uei) cog_agency = lookup_baseline(ein, uei, dbkey) if cog_agency: return cog_agency - (gen_count, total_amount_expended, report_id_2019) = get_2019_gen(ein, uei) + + (gen_count, total_amount_expended, report_id_base_year) = get_base_gen( + ein, uei, base_year + ) if gen_count != 1: return None - cfdas = get_2019_cfdas(report_id_2019) + cfdas = get_base_cfdas(report_id_base_year) if not cfdas: - # logger.warning("Found no cfda data for dbkey {dbkey} in 2019") return None (max_total_agency, max_da_agency) = calc_cfda_amounts(cfdas) cognizant_agency = determine_agency( @@ -102,7 +129,9 @@ def determine_hist_agency(ein, uei): def get_dbkey(ein, uei): try: report_id = General.objects.values_list("report_id", flat=True).get( - Q(auditee_ein=ein), Q(auditee_uei=uei), Q(audit_year="2022") + Q(auditee_ein=ein), + Q(auditee_uei=uei), + Q(audit_year=DBKEY_TO_UEI_TRANSITION_YEAR), ) except (General.DoesNotExist, General.MultipleObjectsReturned): report_id = None @@ -111,7 +140,7 @@ def get_dbkey(ein, uei): try: dbkey = MigrationInspectionRecord.objects.values_list("dbkey", flat=True).get( - Q(report_id=report_id), Q(audit_year="2022") + Q(report_id=report_id), Q(audit_year=DBKEY_TO_UEI_TRANSITION_YEAR) ) except ( MigrationInspectionRecord.DoesNotExist, @@ -136,10 +165,10 @@ def lookup_baseline(ein, uei, dbkey): return cognizant_agency -def get_2019_gen(ein, uei): +def get_base_gen(ein, uei, base_year): gens = General.objects.annotate( amt=Cast("total_amount_expended", output_field=BigIntegerField()) - ).filter(Q(auditee_ein=ein), Q(auditee_uei=uei), Q(audit_year="2019")) + ).filter(Q(auditee_ein=ein), Q(auditee_uei=uei), Q(audit_year=base_year)) if len(gens) != 1: return (len(gens), 0, None) @@ -147,7 +176,7 @@ def get_2019_gen(ein, uei): return (1, gen.amt, gen.report_id) -def get_2019_cfdas(report_id): +def get_base_cfdas(report_id): cfdas = FederalAward.objects.annotate( amt=Cast("amount_expended", output_field=BigIntegerField()) ).filter(Q(report_id=report_id)) @@ -188,7 +217,6 @@ def prune_dict_to_max_values(data: dict): for key, value in data.items(): if value == max_value: pruned_dict[key] = value - return pruned_dict diff --git a/backend/support/test_cog_over.py b/backend/support/test_cog_over.py index 3d6875ef1..61d429125 100644 --- a/backend/support/test_cog_over.py +++ b/backend/support/test_cog_over.py @@ -167,6 +167,7 @@ def _fake_general(): "auditee_fiscal_period_start": "2022-11-01", "user_provided_organization_type": "state", "auditor_ein_not_an_ssn_attestation": "true", + "audit_year": "2023", } @staticmethod @@ -297,7 +298,11 @@ def test_cog_assignment_from_hist(self): sac = self._fake_sac() sac.general_information["ein"] = UNIQUE_EIN_WITHOUT_DBKEY cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, "84") self.assertEqual(over_agency, None) @@ -310,7 +315,11 @@ def test_cog_assignment_with_no_hist(self): sac = self._fake_sac() sac.general_information["ein"] = EIN_2023_ONLY cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, "10") self.assertEqual(over_agency, None) @@ -324,7 +333,11 @@ def test_cog_assignment_with_multiple_hist(self): sac = self._fake_sac() sac.general_information["ein"] = DUP_EIN_WITHOUT_RESOLVER cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, "10") self.assertEqual(over_agency, None) @@ -340,7 +353,11 @@ def test_cog_assignment_with_hist_resolution(self): sac.general_information["ein"] = RESOLVABLE_EIN_WITHOUT_BASELINE sac.general_information["auditee_uei"] = RESOLVABLE_UEI_WITHOUT_BASELINE cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, "22") self.assertEqual(over_agency, None) @@ -355,7 +372,11 @@ def test_over_assignment(self): federal_awards=self._fake_federal_awards_lt_cog_limit(), ) cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, None) self.assertEqual(over_agency, "15") @@ -372,7 +393,11 @@ def test_over_assignment_with_hist(self): ) sac.general_information["ein"] = UNIQUE_EIN_WITHOUT_DBKEY cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, None) self.assertEqual(over_agency, "15") @@ -412,7 +437,11 @@ def test_cog_assignment_with_uei_in_baseline(self): is_active=True, ) cog_agency, over_agency = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) self.assertEqual(cog_agency, BASE_COG) self.assertEqual(over_agency, None) @@ -438,7 +467,11 @@ def test_cog_assignment_with_uei_in_baseline_and_overris(self): self.assertEqual(len(cbs), 1) cog_agency, _ = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) record_cog_assignment(sac.report_id, sac.submitted_by, cog_agency) cas = CognizantAssignment.objects.all() @@ -466,8 +499,147 @@ def test_cog_assignment_with_uei_in_baseline_and_overris(self): sac.cognizant_agency = None sac.save() cog_agency, _ = compute_cog_over( - sac.federal_awards, sac.submission_status, sac.ein, sac.auditee_uei + sac.federal_awards, + sac.submission_status, + sac.ein, + sac.auditee_uei, + sac.general_information["audit_year"], ) record_cog_assignment(sac.report_id, sac.submitted_by, cog_agency) sac = SingleAuditChecklist.objects.get(report_id=sac.report_id) self.assertEqual(sac.cognizant_agency, cog_agency) + + def test_cog_assignment_for_2024_audit(self): + sac = self._fake_sac() + sac.general_information["auditee_uei"] = "ZQGGHJH74DW7" + sac.general_information["ein"] = UNIQUE_EIN_WITHOUT_DBKEY + sac.general_information["report_id"] = "1111-03-GSAFAC-0000202460" + sac.general_information["auditee_fiscal_period_end"] = "2024-05-31" + sac.general_information["auditee_fiscal_period_start"] = "2023-06-01" + sac.report_id = "1111-03-GSAFAC-0000202460" + + gen = baker.make( + General, + report_id="1111-03-GSAFAC-0000202460", + auditee_ein=UNIQUE_EIN_WITHOUT_DBKEY, + auditee_uei="ZQGGHJH74DW7", + total_amount_expended="210000000", + audit_year="2024", + ) + gen.save() + + for i in range(6): + cfda = baker.make( + FederalAward, + report_id=gen, + federal_agency_prefix="84", + federal_award_extension="032", + amount_expended=10_000_000 * i, + is_direct="Y", + ) + cfda.save() + + sac.save() + + cog_agency, over_agency = compute_cog_over( + sac.federal_awards, + sac.submission_status, + sac.general_information["ein"], + sac.general_information["auditee_uei"], + sac.general_information["audit_year"], + ) + self.assertEqual(cog_agency, "84") + self.assertEqual(over_agency, None) + + def test_cog_assignment_for_2027_w_baseline(self): + sac = self._fake_sac() + sac.general_information["auditee_uei"] = "ZQGGHJH74DW7" + sac.general_information["ein"] = UNIQUE_EIN_WITHOUT_DBKEY + sac.general_information["report_id"] = "1111-03-GSAFAC-0000202760" + sac.general_information["auditee_fiscal_period_end"] = "2027-05-31" + sac.general_information["auditee_fiscal_period_start"] = "2026-06-01" + sac.report_id = "1111-03-GSAFAC-0000202760" + + gen = baker.make( + General, + report_id="1111-03-GSAFAC-0000202760", + auditee_ein=UNIQUE_EIN_WITHOUT_DBKEY, + auditee_uei="ZQGGHJH74DW7", + total_amount_expended="210000000", + audit_year="2027", + ) + gen.save() + + for i in range(6): + cfda = baker.make( + FederalAward, + report_id=gen, + federal_agency_prefix="14", + federal_award_extension="032", + amount_expended=10_000_000 * i, + is_direct="Y", + ) + cfda.save() + + sac.save() + + baker.make( + CognizantBaseline, + uei="ZQGGHJH74DW7", + ein=UNIQUE_EIN_WITHOUT_DBKEY, + cognizant_agency="24", + ) + cbs = CognizantBaseline.objects.all() + self.assertEqual(len(cbs), 1) + + cog_agency, over_agency = compute_cog_over( + sac.federal_awards, + sac.submission_status, + sac.general_information["ein"], + sac.general_information["auditee_uei"], + sac.general_information["audit_year"], + ) + self.assertEqual(cog_agency, "24") + self.assertEqual(over_agency, None) + + def test_cog_assignment_for_2027_no_baseline(self): + sac = self._fake_sac() + sac.general_information["auditee_uei"] = "ZQGGHJH74DW8" + sac.general_information["ein"] = "EI27NOBAS" + sac.general_information["report_id"] = "1111-03-GSAFAC-0000202761" + sac.general_information["auditee_fiscal_period_end"] = "2027-05-31" + sac.general_information["auditee_fiscal_period_start"] = "2026-06-01" + sac.report_id = "1111-03-GSAFAC-0000202761" + + gen = baker.make( + General, + report_id="1111-03-GSAFAC-0000202761", + auditee_ein="EI27NOBAS", + auditee_uei="ZQGGHJH74DW8", + total_amount_expended="210000000", + audit_year="2027", + ) + gen.save() + + for i in range(6): + cfda = baker.make( + FederalAward, + report_id=gen, + federal_agency_prefix="10", + federal_award_extension="032", + amount_expended=10_000_000 * i, + is_direct="Y", + ) + cfda.save() + + sac.save() + + cog_agency, over_agency = compute_cog_over( + sac.federal_awards, + sac.submission_status, + sac.general_information["ein"], + sac.general_information["auditee_uei"], + sac.general_information["audit_year"], + ) + self.assertEqual(cog_agency, "10") + self.assertEqual(over_agency, None)