Skip to content

Commit a6f2e9b

Browse files
authored
Merge pull request #5430 from HHS/OPS-5380/fix-agreement-reporting-mismatch
fix: use built in award type instead of custom one for reporting summary
2 parents ffb231b + b6a48de commit a6f2e9b

3 files changed

Lines changed: 139 additions & 103 deletions

File tree

backend/ops_api/ops/utils/reporting_summary.py

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from decimal import Decimal
2-
from typing import Optional
32

43
from sqlalchemy import and_, func, select
54
from sqlalchemy.orm import Session, joinedload
@@ -25,32 +24,6 @@
2524
]
2625

2726

28-
def classify_agreement_for_fy(agreement: Agreement, fiscal_year: int) -> Optional[str]:
29-
"""
30-
Classify agreement as NEW, CONTINUING, or None for a given fiscal year.
31-
32-
Replicates Agreement.award_type property logic but accepts explicit fiscal year.
33-
"""
34-
has_non_draft_blis = any(
35-
bli.status is not None and bli.status != BudgetLineItemStatus.DRAFT for bli in agreement.budget_line_items
36-
)
37-
38-
if not has_non_draft_blis:
39-
return None
40-
41-
if not agreement.is_awarded:
42-
return AgreementClassification.NEW.name
43-
44-
award_fy = agreement.award_fiscal_year
45-
if award_fy is None:
46-
return AgreementClassification.NEW.name
47-
48-
if fiscal_year <= award_fy:
49-
return AgreementClassification.NEW.name
50-
51-
return AgreementClassification.CONTINUING.name
52-
53-
5427
def _find_bucket_type(agreement_type):
5528
"""Find the spending bucket type for a given agreement type."""
5629
for config in AGREEMENT_TYPE_CONFIG:
@@ -65,7 +38,7 @@ def _accumulate_agreement_spending(agreement, fiscal_year, totals, portfolio_ids
6538
if bucket_type is None:
6639
return
6740

68-
classification = classify_agreement_for_fy(agreement, fiscal_year)
41+
classification = agreement.award_type # Use award_type property for consistency with Agreements list
6942
if classification is None:
7043
return
7144

@@ -205,7 +178,7 @@ def get_reporting_counts(session: Session, fiscal_year: int, portfolio_ids=None)
205178
continue
206179
agreement_counts[bucket_type] += 1
207180

208-
classification = classify_agreement_for_fy(agreement, fiscal_year)
181+
classification = agreement.award_type
209182
if classification == AgreementClassification.NEW.name:
210183
new_counts[bucket_type] += 1
211184
elif classification == AgreementClassification.CONTINUING.name:

backend/ops_api/tests/ops/funding_summary/test_agreement_spending_summary.py

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,92 +5,20 @@
55
import pytest
66

77
from models import (
8-
Agreement,
8+
AgreementClassification,
99
AgreementType,
1010
BudgetLineItemStatus,
1111
ContractAgreement,
1212
ContractBudgetLineItem,
1313
GrantAgreement,
1414
GrantBudgetLineItem,
1515
)
16-
from models.agreements import AgreementClassification
1716
from ops_api.ops.utils.reporting_summary import (
1817
_accumulate_agreement_spending,
19-
classify_agreement_for_fy,
2018
get_agreement_spending_by_type,
2119
)
2220

2321

24-
class TestClassifyAgreementForFy:
25-
def test_no_non_draft_blis_returns_none(self):
26-
agreement = MagicMock(spec=Agreement)
27-
draft_bli = MagicMock()
28-
draft_bli.status = BudgetLineItemStatus.DRAFT
29-
agreement.budget_line_items = [draft_bli]
30-
31-
result = classify_agreement_for_fy(agreement, 2025)
32-
assert result is None
33-
34-
def test_empty_blis_returns_none(self):
35-
agreement = MagicMock(spec=Agreement)
36-
agreement.budget_line_items = []
37-
38-
result = classify_agreement_for_fy(agreement, 2025)
39-
assert result is None
40-
41-
def test_not_awarded_returns_new(self):
42-
agreement = MagicMock(spec=Agreement)
43-
planned_bli = MagicMock()
44-
planned_bli.status = BudgetLineItemStatus.PLANNED
45-
agreement.budget_line_items = [planned_bli]
46-
agreement.is_awarded = False
47-
48-
result = classify_agreement_for_fy(agreement, 2025)
49-
assert result == AgreementClassification.NEW.name
50-
51-
def test_awarded_no_award_date_returns_new(self):
52-
agreement = MagicMock(spec=Agreement)
53-
planned_bli = MagicMock()
54-
planned_bli.status = BudgetLineItemStatus.PLANNED
55-
agreement.budget_line_items = [planned_bli]
56-
agreement.is_awarded = True
57-
agreement.award_fiscal_year = None
58-
59-
result = classify_agreement_for_fy(agreement, 2025)
60-
assert result == AgreementClassification.NEW.name
61-
62-
def test_awarded_current_fy_lte_award_fy_returns_new(self):
63-
agreement = MagicMock(spec=Agreement)
64-
planned_bli = MagicMock()
65-
planned_bli.status = BudgetLineItemStatus.PLANNED
66-
agreement.budget_line_items = [planned_bli]
67-
agreement.is_awarded = True
68-
agreement.award_fiscal_year = 2025
69-
70-
result = classify_agreement_for_fy(agreement, 2025)
71-
assert result == AgreementClassification.NEW.name
72-
73-
def test_awarded_current_fy_gt_award_fy_returns_continuing(self):
74-
agreement = MagicMock(spec=Agreement)
75-
planned_bli = MagicMock()
76-
planned_bli.status = BudgetLineItemStatus.PLANNED
77-
agreement.budget_line_items = [planned_bli]
78-
agreement.is_awarded = True
79-
agreement.award_fiscal_year = 2024
80-
81-
result = classify_agreement_for_fy(agreement, 2025)
82-
assert result == AgreementClassification.CONTINUING.name
83-
84-
def test_bli_with_none_status_ignored(self):
85-
agreement = MagicMock(spec=Agreement)
86-
none_bli = MagicMock()
87-
none_bli.status = None
88-
agreement.budget_line_items = [none_bli]
89-
90-
result = classify_agreement_for_fy(agreement, 2025)
91-
assert result is None
92-
93-
9422
@pytest.fixture()
9523
def db_with_agreement_spending_data(app, loaded_db, app_ctx):
9624
from models import CAN, CANFundingBudget, CANFundingDetails, Portfolio, ResearchProject
@@ -410,7 +338,7 @@ def test_processes_mix_of_blis_with_and_without_can(self):
410338
"""Only BLIs with a CAN should contribute to totals."""
411339
agreement = MagicMock()
412340
agreement.agreement_type = AgreementType.CONTRACT
413-
agreement.is_awarded = False
341+
agreement.award_type = AgreementClassification.NEW.name
414342

415343
bli_no_can = MagicMock()
416344
bli_no_can.fiscal_year = 2025

backend/ops_api/tests/ops/utils/test_reporting_counts.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
from datetime import date
22
from decimal import Decimal
3+
from unittest.mock import patch
34

45
import pytest
56

67
from models import (
8+
CAN,
79
AgreementType,
810
BudgetLineItemStatus,
11+
CANFundingBudget,
12+
CANFundingDetails,
913
ContractAgreement,
1014
ContractBudgetLineItem,
1115
GrantAgreement,
1216
GrantBudgetLineItem,
17+
Portfolio,
18+
ResearchProject,
1319
)
1420
from ops_api.ops.utils.reporting_summary import get_reporting_counts
1521

@@ -202,3 +208,132 @@ def test_get_reporting_counts_empty_fy(app, loaded_db, app_ctx):
202208
assert result["new_agreements"]["total"] == 0
203209
assert result["continuing_agreements"]["total"] == 0
204210
assert result["budget_lines"]["total"] == 0
211+
212+
213+
@pytest.fixture()
214+
def db_with_2024_data(app, loaded_db, app_ctx):
215+
"""Fixture with FY 2024 data for testing with mocked get_current_fiscal_year."""
216+
217+
portfolio = Portfolio(name="FY2024 TEST PORTFOLIO", division_id=1)
218+
can = CAN(number="FY2024_TEST_CAN")
219+
portfolio.cans.append(can)
220+
loaded_db.add(portfolio)
221+
loaded_db.commit()
222+
223+
can_funding_details = CANFundingDetails(fiscal_year=2024, fund_code="FY2024TEST")
224+
can.funding_details = can_funding_details
225+
loaded_db.add(can_funding_details)
226+
loaded_db.commit()
227+
228+
can_funding_budget = CANFundingBudget(can_id=can.id, fiscal_year=2024, budget=Decimal(30000000))
229+
loaded_db.add(can_funding_budget)
230+
loaded_db.commit()
231+
232+
# Research project
233+
project = ResearchProject(title="FY2024 Test Project", short_title="FY24TP", description="Test project for FY 2024")
234+
loaded_db.add(project)
235+
loaded_db.commit()
236+
237+
# Contract agreement with PLANNED BLI in FY 2024
238+
contract = ContractAgreement(name="FY2024 Contract", agreement_type=AgreementType.CONTRACT, project_id=project.id)
239+
loaded_db.add(contract)
240+
loaded_db.commit()
241+
242+
bli_planned = ContractBudgetLineItem(
243+
line_description="FY2024 Planned BLI",
244+
amount=Decimal("8000000"),
245+
status=BudgetLineItemStatus.PLANNED,
246+
can_id=can.id,
247+
date_needed=date(2024, 3, 15),
248+
agreement_id=contract.id,
249+
)
250+
loaded_db.add(bli_planned)
251+
loaded_db.commit()
252+
253+
# Grant agreement with OBLIGATED BLI in FY 2024
254+
grant = GrantAgreement(name="FY2024 Grant", agreement_type=AgreementType.GRANT, project_id=project.id)
255+
loaded_db.add(grant)
256+
loaded_db.commit()
257+
258+
bli_obligated = GrantBudgetLineItem(
259+
line_description="FY2024 Obligated BLI",
260+
amount=Decimal("3000000"),
261+
status=BudgetLineItemStatus.OBLIGATED,
262+
can_id=can.id,
263+
date_needed=date(2024, 8, 1),
264+
agreement_id=grant.id,
265+
)
266+
loaded_db.add(bli_obligated)
267+
loaded_db.commit()
268+
269+
# Contract with IN_EXECUTION BLI in FY 2024
270+
contract_exec = ContractAgreement(
271+
name="FY2024 Executing Contract", agreement_type=AgreementType.CONTRACT, project_id=project.id
272+
)
273+
loaded_db.add(contract_exec)
274+
loaded_db.commit()
275+
276+
bli_in_execution = ContractBudgetLineItem(
277+
line_description="FY2024 In Execution BLI",
278+
amount=Decimal("2000000"),
279+
status=BudgetLineItemStatus.IN_EXECUTION,
280+
can_id=can.id,
281+
date_needed=date(2024, 5, 1),
282+
agreement_id=contract_exec.id,
283+
)
284+
loaded_db.add(bli_in_execution)
285+
loaded_db.commit()
286+
287+
yield loaded_db
288+
289+
loaded_db.rollback()
290+
for obj in [
291+
bli_in_execution,
292+
contract_exec,
293+
bli_obligated,
294+
grant,
295+
bli_planned,
296+
contract,
297+
project,
298+
can_funding_budget,
299+
can_funding_details,
300+
can,
301+
portfolio,
302+
]:
303+
loaded_db.delete(obj)
304+
loaded_db.commit()
305+
306+
307+
@patch("models.utils.fiscal_year.get_current_fiscal_year", return_value=2024)
308+
def test_get_reporting_counts_with_mocked_fy_2024(mock_fy, app, db_with_2024_data, app_ctx):
309+
"""Test get_reporting_counts with mocked get_current_fiscal_year returning 2024."""
310+
# Verify the mock is working
311+
# assert get_current_fiscal_year() == 2024
312+
313+
# Get reporting counts for FY 2024
314+
result = get_reporting_counts(app.db_session, 2024)
315+
316+
# Verify structure
317+
assert "projects" in result
318+
assert "agreements" in result
319+
assert "new_agreements" in result
320+
assert "continuing_agreements" in result
321+
assert "budget_lines" in result
322+
323+
# Verify project counts - we created 1 research project
324+
assert result["projects"]["total"] >= 1
325+
project_types = {t["type"]: t["count"] for t in result["projects"]["types"]}
326+
assert project_types.get("RESEARCH", 0) >= 1
327+
328+
# Verify agreement counts - we created 2 contracts and 1 grant with non-DRAFT BLIs
329+
assert result["agreements"]["total"] >= 3
330+
agreement_types = {t["type"]: t["count"] for t in result["agreements"]["types"]}
331+
assert agreement_types.get("CONTRACT", 0) >= 2 # 2 contracts
332+
assert agreement_types.get("GRANT", 0) >= 1 # 1 grant
333+
334+
# Verify budget line counts - we created 3 BLIs: PLANNED, OBLIGATED, IN_EXECUTION
335+
assert result["budget_lines"]["total"] >= 3
336+
bli_types = {t["type"]: t["count"] for t in result["budget_lines"]["types"]}
337+
assert bli_types.get("PLANNED", 0) >= 1
338+
assert bli_types.get("OBLIGATED", 0) >= 1
339+
assert bli_types.get("IN_EXECUTION", 0) >= 1

0 commit comments

Comments
 (0)