Skip to content

4745 implement start past report - #4783

Merged
shon-button merged 10 commits into
developfrom
feature/4745-past-year-reporting
Jul 9, 2026
Merged

4745 implement start past report#4783
shon-button merged 10 commits into
developfrom
feature/4745-past-year-reporting

Conversation

@shon-button

@shon-button shon-button commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Ticket: 4745

🚀 Impact

  • Implemented previous reporting year report creation based on reportable operations for each reporting year, with registration purposes determined by the selected operation type and validated for required report information during report creation
  • Updated dashboard and grid reporting labels

🔬 Local Testing:

Start Test Environment

  1. Start the API server:
    cd bc_obps
    make prepare_backend
  2. Start the app development server:
    cd bciers && yarn dev-all

Test: Start a previous report

  1. Log in using bc-cas-dev
  2. Navigate to http://localhost:3000/reporting/reports
    Expected Results:
  • Tab Current Reporting Year
  • Tab Previous Years Reporting
image
  1. Navigate to Previous Years Reporting
  2. Click button File Previous Years Report
    Expected Results:
  • Start report form displays as expected
image
  1. Select reporting year= 2024 (only)
  2. Select an operation
  3. Select registration purpose
  4. Click Start
    Expected Results:
  • Review Operation Information displays as expected
  • There is no Sync buton on Review Operation Information, Review Facilities, and Review Facility Information page
image
  1. Submit the report
    Expected Results:
  • Report submits as expected
image
  1. Create supplementary report
  2. Submit the report
    Expected Results:
  • Report submits as expected

Test: Cannot start a previous report

  1. Repeat Test: Start a previous report 1-8 with operation Brine LFO - Registered - No BORO and BCGHG ID - name from admin`
    Expected Results:
  • User error message displays
image

@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 12 times, most recently from b2a0d34 to 1fa7e41 Compare June 17, 2026 17:02
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch from 42646bf to efb06c3 Compare June 19, 2026 17:31
@shon-button
shon-button marked this pull request as draft June 19, 2026 17:37
@shon-button
shon-button marked this pull request as ready for review June 22, 2026 16:39
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 4 times, most recently from 9814659 to d4322d3 Compare June 23, 2026 17:57
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 2 times, most recently from 6d92528 to 2f92d69 Compare July 2, 2026 14:50
@shon-button shon-button added the run-all-ci Trigger label to force all CI to run on a PR. label Jul 2, 2026
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 6 times, most recently from 80e9f4d to e055402 Compare July 3, 2026 17:16

@Sepehr-Sobhani Sepehr-Sobhani left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Impressive work! 🤩

Comment on lines +18 to +50
vi.mock("@bciers/components/form/FormBase", () => ({
default: vi.fn(({ children, onChange, onSubmit }) => (
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit({
formData: {
reporting_year: 2023,
operation_id: "operation-1",
registration_purpose: "Reporting Operation",
},
});
}}
>
<button
type="button"
onClick={() =>
onChange({
formData: {
reporting_year: 2023,
operation_id: "operation-1",
registration_purpose: "Reporting Operation",
},
})
}
>
Mock Change
</button>

{children}
</form>
)),
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to mock FormBase? I think we could just let the test render without mocking.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: We have a couple of any types in this file. It would be great if we could replace them with the appropriate types.

Comment thread bc_obps/service/report_service.py Outdated
# the designated operator for the selected reporting year
if designated_operator_timeline.operator.id != user_operator.operator_id:
raise UserError(
f"This operation was owned by another operation in {reporting_year}, "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

owned by another operation operator

Comment thread bc_obps/service/operation_service.py Outdated
Comment on lines +782 to +794
def _get_previous_reporting_years(cls) -> QuerySet[ReportingYear]:
"""
Returns reporting years that are eligible for the Start Past Report workflow

Only reporting years from MIN_REPORTING_YEAR up to (but not including) the current
reporting year are returned.
"""
current_reporting_year = ReportingYearService.get_current_reporting_year()

return ReportingYear.objects.filter(
reporting_year__gte=cls.MIN_REPORTING_YEAR,
reporting_year__lt=current_reporting_year.reporting_year,
).order_by("-reporting_year")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we move this to ReportingYearService?

Comment thread bc_obps/service/report_service.py Outdated
def create_report_for_reporting_year(
cls,
user_guid: UUID,
data: Any,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A dataclass instead of Any?

Comment on lines +500 to +501
operator = operator_baker()
operation = operation_baker(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We have some existing operator_baker() and operation_baker() helpers from before we decided to standardize on recipes. I think it would be better to use recipes here as well.

For the existing usages, we can replace them with recipes in a separate tech debt ticket. I'll make a note to create one. 😃

Comment on lines -24 to +30
{ label: "View annual report(s)", href: "/reports/current-reports" },
{ label: "View past reports", href: "/reports/previous-years" },
{ label: "Current Reporting Year", href: "/reports/current-reports" },
{ label: "Previous Reporting Years", href: "/reports/previous-years" },
]
: [
//tabs for internal users
{ label: "View annual reports", href: "/reports/current-reports" },
{ label: "View past reports", href: "/reports/previous-years" },
{ label: "Current Reporting Year", href: "/reports/current-reports" },
{ label: "Previous Reporting Years", href: "/reports/previous-years" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since we're changing these labels, should we also update the ones on the dashboard to keep them in sync?

@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 6 times, most recently from 92dd9a5 to 0d68f4e Compare July 7, 2026 16:44
Comment on lines +4 to +19
def load_dashboard_fixtures(apps, schema_editor):
from django.core.management import call_command

fixture_files = [
'common/fixtures/dashboard/bciers/external.json',
'common/fixtures/dashboard/bciers/internal.json',
'common/fixtures/dashboard/operators/internal.json',
]

# Delete all existing DashboardData objects
DashboardData = apps.get_model('common', 'DashboardData')
DashboardData.objects.all().delete()

# Load the fixtures
for fixture in fixture_files:
call_command('loaddata', fixture)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already have a util for it:

def reset_dashboard_data() -> None:

@pbastia pbastia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Started going through it and just left a bunch of mostly nitpicks that don't necessarily need to be addressed. Great work so far!

Comment on lines +4 to +19
def load_dashboard_fixtures(apps, schema_editor):
from django.core.management import call_command

fixture_files = [
'common/fixtures/dashboard/bciers/external.json',
'common/fixtures/dashboard/bciers/internal.json',
'common/fixtures/dashboard/operators/internal.json',
]

# Delete all existing DashboardData objects
DashboardData = apps.get_model('common', 'DashboardData')
DashboardData.objects.all().delete()

# Load the fixtures
for fixture in fixture_files:
call_command('loaddata', fixture)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code already exists: reset_dashboard_data()in common/utils.py
The migration could just call that function in its RunPython method


class TestOperationsPreviousReportableEndpoint(CommonTestSetup):
@patch("service.operation_service.OperationService.list_previous_reportable_operations")
def test_list_previous_reportable_operations(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: method name

Suggested change
def test_list_previous_reportable_operations(
def test_calls_and_returns_the_service_method_output(

Comment thread bc_obps/registration/tests/endpoints/_operations/test_reportable.py
Comment thread bc_obps/reporting/tests/api/test_reports.py
reporting_years = ReportingYearService.get_previous_reporting_years()

# Assert
assert list(reporting_years.values_list("reporting_year", flat=True)) == [2024]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: we could use assertQuerySetEquals, pytest exposes them too if we don't want to extend TestCase

Comment on lines +138 to +152
empty_result = (
OperationDesignatedOperatorTimelineService.get_operation_designated_operators_for_reporting_years(
operation_ids=set(),
reporting_years={2024, 2025},
)
)
assert empty_result == {}

empty_result = (
OperationDesignatedOperatorTimelineService.get_operation_designated_operators_for_reporting_years(
operation_ids={operation.id},
reporting_years=set(),
)
)
assert empty_result == {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: these could be individual test cases

ordered=False,
)

def test_create_report_for_reporting_year_uses_selected_registration_purpose(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: maybe we can split those tests in its own testfile, for readability. This one is getting really really long

"Only currently registered operations can be used to create a report for this reporting year.",
)

def test_create_report_for_reporting_year_rejects_existing_report(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: it seems like these tests are all setup the same way, they would be good candidates for parametrization

Comment on lines +106 to +107
if start_date is None:
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

elements with a None start_date have been filtered out just above, we can remove those two lines

Comment on lines +84 to +85
min_year = min(reporting_years)
max_year = max(reporting_years)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This assumes a continuous set of reporting years, otherwise the method will behave weirdly for disjointed sets. I suggest having the user pass min_year and max_year as parameters instead.

@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch from 5e6801a to a46fc97 Compare July 8, 2026 13:37
@shon-button shon-button added run-all-ci Trigger label to force all CI to run on a PR. and removed run-all-ci Trigger label to force all CI to run on a PR. labels Jul 8, 2026
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 2 times, most recently from 6ae6a30 to 308b461 Compare July 8, 2026 15:35

@pbastia pbastia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few more things, but this is great! Approved once the non-nit comments are addressed.
Very nice work!



class ReportingYearService:
MIN_REPORTING_YEAR = 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hardcoding this defeats the purpose of storing valid reporting years in the database (I'll make a separate PR to remove 2023 that shouldn't be there). Then we can just filter "All reporting years before the current one"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: URL path is a bit redundant, I suggest "/reports/previous-years/start" or "/reports/previous-years/create"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@pbastia

The route path is to match the page title to the breadcrumb:
image

Comment on lines +36 to +43
try {
const finalReviewData = await getFinalReviewData(version_id);
setData(finalReviewData);
} catch (error) {
console.error("FinalReviewForm.fetchData() failed", error);
setError(error instanceof Error ? error : new Error(String(error)));
} finally {
setLoading(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We might want to raise an error instead to have our error boundary catch it and notify Sentry, otherwise this just disappears and relies on users emailing us about it

<div className="flex justify-start gap-3 pt-6">
<Button
variant="outlined"
onClick={() => router.back()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should probably navigate to the past reports page, independently of how the user got here

{
body: JSON.stringify(payload),
},
)) as CreateReportResponse | number;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The naming makes it sound like the "CreateReportResponse" is the valid, nominal case

Suggested change
)) as CreateReportResponse | number;
)) as CreateReportErrorResponse | number;

const mockActionHandler = vi.mocked(actionHandler);
const mockUseRouter = vi.mocked(useRouter);

const schema: RJSFSchema = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

using a test schema for the component has us testing RJSF internals, just verifying that it displays a test schema properly.
I suggest using the real generated schema, this way we'll catch regressions in functionality

Comment thread bc_obps/service/operation_service.py Outdated
Comment on lines +818 to +826
@classmethod
def _add_reportable_operation(
cls,
reportable_operations: list[dict],
added_operation_years: set[tuple[UUID, int]],
operation: Operation,
reporting_year: ReportingYear,
is_current_registered_fallback: bool,
) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: coding style, particularly in class or static methods, we should try avoid functions that modify data structures in place and act as a side-effect.

Instead, the function should take inputs, treat them as immutable, and return new outputs

chore: cleanup

chore: cleanup

chore: add registration purpose to startReport schema

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup
chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup
chore: cleanup

chore:  cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch 2 times, most recently from 70dd94e to 97967b8 Compare July 9, 2026 13:54
chore: cleanup

chore: cleanup

chore: cleanup

chore: cleanup
@shon-button
shon-button force-pushed the feature/4745-past-year-reporting branch from db7b2ba to 8fe5d82 Compare July 9, 2026 14:52
@shon-button
shon-button merged commit a1f2811 into develop Jul 9, 2026
41 checks passed
@shon-button
shon-button deleted the feature/4745-past-year-reporting branch July 9, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-all-ci Trigger label to force all CI to run on a PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants