diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000000..efc0bdb07a --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +multi_line_output=3 diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 0996e17a40..900de12e19 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -1,17 +1,15 @@ import datetime import json +import uuid import pytest import pytz +from dateutil.parser import parse from django.conf import settings from django.urls import reverse +from freezegun import freeze_time from moto import mock_aws -from pytest_bdd import ( - given, - parsers, - then, - when, -) +from pytest_bdd import given, parsers, then, when from rest_framework import status from rest_framework.test import APIClient @@ -23,46 +21,30 @@ PartyOnApplicationFactory, StandardApplicationFactory, ) -from api.cases.enums import ( - AdviceLevel, - AdviceType, - CaseTypeEnum, -) -from api.cases.models import CaseType +from api.cases.celery_tasks import update_cases_sla +from api.cases.enums import AdviceLevel, AdviceType, CaseTypeEnum +from api.cases.models import CaseType, LicenceDecision from api.cases.tests.factories import FinalAdviceFactory -from api.core.constants import ( - ExporterPermissions, - GovPermissions, - Roles, -) +from api.core.constants import ExporterPermissions, GovPermissions, Roles from api.documents.libraries.s3_operations import init_s3_client from api.flags.enums import SystemFlags from api.goods.tests.factories import GoodFactory from api.letter_templates.models import LetterTemplate from api.licences.enums import LicenceStatus -from api.licences.tests.factories import ( - GoodOnLicenceFactory, - StandardLicenceFactory, -) +from api.licences.models import Licence +from api.licences.tests.factories import GoodOnLicenceFactory, StandardLicenceFactory from api.organisations.tests.factories import OrganisationFactory +from api.parties.enums import PartyType from api.parties.tests.factories import PartyDocumentFactory +from api.staticdata.countries.models import Country from api.staticdata.letter_layouts.models import LetterLayout -from api.staticdata.report_summaries.models import ( - ReportSummaryPrefix, - ReportSummarySubject, -) +from api.staticdata.report_summaries.models import ReportSummaryPrefix, ReportSummarySubject from api.staticdata.statuses.enums import CaseStatusEnum from api.staticdata.statuses.models import CaseStatus from api.staticdata.units.enums import Units -from api.users.enums import ( - SystemUser, - UserType, -) +from api.users.enums import SystemUser, UserType from api.users.libraries.user_to_token import user_to_token -from api.users.models import ( - BaseUser, - Permission, -) +from api.users.models import BaseUser, Permission from api.users.tests.factories import ( BaseUserFactory, ExporterUserFactory, @@ -316,13 +298,13 @@ def api_client(): @pytest.fixture() -def unpage_data(client): +def unpage_data(api_client): def _unpage_data(url): unpaged_results = [] while True: - response = client.get(url) + response = api_client.get(url) assert response.status_code == status.HTTP_200_OK - unpaged_results += response.data["results"] + unpaged_results += response.json()["results"] if not response.data["next"]: break url = response.data["next"] @@ -332,6 +314,18 @@ def _unpage_data(url): return _unpage_data +@pytest.fixture() +def parse_attributes(parse_table): + def _parse_attributes(attributes): + kwargs = {} + table_data = parse_table(attributes) + for key, value in table_data[1:]: + kwargs[key] = value + return kwargs + + return _parse_attributes + + @pytest.fixture() def standard_application(): application = StandardApplicationFactory( @@ -351,6 +345,15 @@ def draft_application(): return draft_application +def run_processing_time_task(start, up_to): + processing_time_task_run_date_time = start.replace(hour=22, minute=30) + up_to = pytz.utc.localize(datetime.datetime.fromisoformat(up_to)) + while processing_time_task_run_date_time <= up_to: + with freeze_time(processing_time_task_run_date_time): + update_cases_sla() + processing_time_task_run_date_time = processing_time_task_run_date_time + datetime.timedelta(days=1) + + @pytest.fixture def submit_application(api_client, exporter_headers, mocker): def _submit_application(draft_application): @@ -394,6 +397,20 @@ def given_draft_standard_application(organisation): return application +@given(parsers.parse("a consignee added to the application in `{country}`")) +def add_consignee_to_application(draft_standard_application, country): + consignee = draft_standard_application.parties.get(party__type=PartyType.CONSIGNEE) + consignee.party.country = Country.objects.get(name=country) + consignee.party.save() + + +@given(parsers.parse("an end-user added to the application of `{country}`")) +def add_end_user_to_application(draft_standard_application, country): + end_user = draft_standard_application.parties.get(party__type=PartyType.END_USER) + end_user.party.country = Country.objects.get(name=country) + end_user.party.save() + + @when( "the application is submitted", target_fixture="submitted_standard_application", @@ -439,13 +456,19 @@ def cast_to_types(data, fields_metadata): for row in data: cast_row = row.copy() for key, value in cast_row.items(): + if not value: + continue field_metadata = fields_metadata[key] if value == "NULL": cast_row[key] = None elif field_metadata["type"] == "Integer": cast_row[key] = int(value) + elif field_metadata["type"] == "Float": + cast_row[key] = float(value) elif field_metadata["type"] == "DateTime": - cast_row[key] = pytz.utc.localize(datetime.datetime.fromisoformat(value)) + cast_row[key] = pytz.utc.localize(parse(value, ignoretz=True)) + elif field_metadata["type"] == "UUID": + cast_row[key] = uuid.UUID(value) if value != "None" else None cast_data.append(cast_row) return cast_data @@ -464,18 +487,27 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): pytest.fail(f"No table called {table_name} found") actual_data = unpage_data(table_metadata["endpoint"]) + actual_data = cast_to_types(actual_data, table_metadata["fields"]) parsed_rows = parse_table(rows) keys = parsed_rows[0] expected_data = [] for row in parsed_rows[1:]: expected_data.append({key: value for key, value in zip(keys, row)}) expected_data = cast_to_types(expected_data, table_metadata["fields"]) - - actual_data = sorted(actual_data, key=lambda d, key=keys[0]: d[key]) - expected_data = sorted(expected_data, key=lambda d, key=keys[0]: d[key]) + actual_data = sorted(actual_data, key=lambda item, keys=keys: item[keys[0]]) + expected_data = sorted(expected_data, key=lambda item, keys=keys: item[keys[0]]) assert actual_data == expected_data +@when( + parsers.parse("the application is submitted at {submission_time}"), + target_fixture="submitted_standard_application", +) +def when_the_application_is_submitted_at(submit_application, draft_standard_application, submission_time): + with freeze_time(submission_time): + return submit_application(draft_standard_application) + + @given(parsers.parse("LITE exports `{table_name}` data to DW")) def given_endpoint_exists(client, table_name): metadata_url = reverse("data_workspace:v2:table-metadata") @@ -486,12 +518,18 @@ def given_endpoint_exists(client, table_name): @given(parsers.parse("the application has the following goods:{goods}")) def given_the_application_has_the_following_goods(parse_table, draft_standard_application, goods): draft_standard_application.goods.all().delete() - good_attributes = parse_table(goods)[1:] - for id, name in good_attributes: + + good_attributes = parse_table(goods) + keys = good_attributes[0] + for row in good_attributes[1:]: + data = dict(zip(keys, row)) GoodOnApplicationFactory( application=draft_standard_application, - id=id, - good__name=name, + id=data["id"], + good__name=data["name"], + quantity=float(data.get("quantity", "10.0")), + unit=data.get("unit", "NAR"), + value=float(data.get("value", "100.0")), ) @@ -546,18 +584,6 @@ def when_the_goods_are_assessed_by_tau( assert response.status_code == 200, response.content -@pytest.fixture() -def parse_attributes(parse_table): - def _parse_attributes(attributes): - kwargs = {} - table_data = parse_table(attributes) - for key, value in table_data[1:]: - kwargs[key] = value - return kwargs - - return _parse_attributes - - @given( parsers.parse("a draft standard application with attributes:{attributes}"), target_fixture="draft_standard_application", @@ -636,6 +662,9 @@ def _refuse_application(application, denial_reasons=None): if not denial_reasons: denial_reasons = ["1a", "1b", "1c"] + # delete previous final advice if any before we change decision + application.advice.filter(level=AdviceLevel.FINAL).delete() + data = {"action": AdviceType.REFUSE} for good_on_app in application.goods.all(): good_on_app.quantity = 100 @@ -681,3 +710,208 @@ def _refuse_application(application, denial_reasons=None): application.refresh_from_db() return _refuse_application + + +@when(parsers.parse("the application is issued at {timestamp}"), target_fixture="issued_application") +def when_the_application_is_issued_at( + issue_licence, + submitted_standard_application, + timestamp, + mocker, +): + run_processing_time_task(submitted_standard_application.submitted_at, timestamp) + + def mock_licence_save(self, *args, send_status_change_to_hmrc=False, **kwargs): + self.id = "1b2f95c3-9cd2-4dee-b134-a79786f78c06" + self.end_date = datetime.datetime.now().date() + super(Licence, self).save(*args, **kwargs) + + mocker.patch.object(Licence, "save", mock_licence_save) + + def mock_licence_decision_save(self, *args, **kwargs): + self.id = "ebd27511-7be3-4e5c-9ce9-872ad22811a1" + super(LicenceDecision, self).save(*args, **kwargs) + + mocker.patch.object(LicenceDecision, "save", mock_licence_decision_save) + + with freeze_time(timestamp): + issue_licence(submitted_standard_application) + + submitted_standard_application.refresh_from_db() + issued_application = submitted_standard_application + + return issued_application + + +@when(parsers.parse("the application is refused at {timestamp}"), target_fixture="refused_application") +def when_the_application_is_refused_at( + submitted_standard_application, + refuse_application, + timestamp, + mocker, +): + run_processing_time_task(submitted_standard_application.submitted_at, timestamp) + + def mock_licence_decision_refuse(self, *args, **kwargs): + self.id = "4ea4261f-03f2-4baf-8784-5ec4b352d358" + super(LicenceDecision, self).save(*args, **kwargs) + + mocker.patch.object(LicenceDecision, "save", mock_licence_decision_refuse) + + with freeze_time(timestamp): + refuse_application(submitted_standard_application) + + submitted_standard_application.refresh_from_db() + refused_application = submitted_standard_application + return refused_application + + +@when(parsers.parse("the issued application is revoked at {timestamp}")) +def when_the_issued_application_is_revoked( + api_client, + lu_sr_manager_headers, + issued_application, + timestamp, + mocker, +): + run_processing_time_task(issued_application.submitted_at, timestamp) + + def mock_licence_decision_revoke(self, *args, **kwargs): + self.id = "65ad0aa8-64ad-4805-92f1-86a4874e9fe6" + super(LicenceDecision, self).save(*args, **kwargs) + + mocker.patch.object(LicenceDecision, "save", mock_licence_decision_revoke) + + with freeze_time(timestamp): + issued_licence = issued_application.licences.get() + url = reverse("licences:licence_details", kwargs={"pk": str(issued_licence.pk)}) + response = api_client.patch( + url, + data={"status": LicenceStatus.REVOKED}, + **lu_sr_manager_headers, + ) + assert response.status_code == 200, response.status_code + + +@when(parsers.parse("the application is appealed at {timestamp}"), target_fixture="appealed_application") +def when_the_application_is_appealed_at( + refused_application, + api_client, + exporter_headers, + timestamp, +): + with freeze_time(timestamp): + response = api_client.post( + reverse( + "applications:appeals", + kwargs={ + "pk": refused_application.pk, + }, + ), + data={ + "grounds_for_appeal": "This is appealing", + }, + **exporter_headers, + ) + assert response.status_code == 201, response.content + + refused_application.refresh_from_db() + appealed_application = refused_application + + return appealed_application + + +@pytest.fixture() +def caseworker_change_status(api_client, lu_case_officer, lu_case_officer_headers): + def _caseworker_change_status(application, status): + url = reverse( + "caseworker_applications:change_status", + kwargs={ + "pk": str(application.pk), + }, + ) + response = api_client.post( + url, + data={"status": status}, + **lu_case_officer_headers, + ) + assert response.status_code == 200, response.content + application.refresh_from_db() + assert application.status.status == status + + return _caseworker_change_status + + +@when(parsers.parse("the refused application is issued on appeal at {timestamp}"), target_fixture="issued_application") +def when_the_application_is_issued_on_appeal_at( + appealed_application, + timestamp, + caseworker_change_status, + issue_licence, + mocker, +): + run_processing_time_task(appealed_application.appeal.created_at, timestamp) + + def mock_licence_save_on_appeal(self, *args, send_status_change_to_hmrc=False, **kwargs): + self.id = "4106ced1-b2b9-41e8-ad42-47c36b07b345" # /PS-IGNORE + self.end_date = datetime.datetime.now().date() + super(Licence, self).save(*args, **kwargs) + + mocker.patch.object(Licence, "save", mock_licence_save_on_appeal) + + def mock_licence_decision_appeal(self, *args, **kwargs): + self.id = "f0bc0c1e-c9c5-4a90-b4c8-81a7f3cbe1e7" # /PS-IGNORE + super(LicenceDecision, self).save(*args, **kwargs) + + mocker.patch.object(LicenceDecision, "save", mock_licence_decision_appeal) + + with freeze_time(timestamp): + appealed_application.advice.filter(level=AdviceLevel.FINAL).update( + type=AdviceType.APPROVE, + text="issued on appeal", + ) + + caseworker_change_status(appealed_application, CaseStatusEnum.REOPENED_FOR_CHANGES) + caseworker_change_status(appealed_application, CaseStatusEnum.UNDER_FINAL_REVIEW) + issue_licence(appealed_application) + + appealed_application.refresh_from_db() + issued_application = appealed_application + + return issued_application + + +@when(parsers.parse("the application is reissued at {timestamp}")) +def when_the_application_is_issued_again_at( + issued_application, + timestamp, + caseworker_change_status, + issue_licence, + mocker, +): + run_processing_time_task(issued_application.appeal.created_at, timestamp) + + def mock_licence_save_reissue(self, *args, send_status_change_to_hmrc=False, **kwargs): + if self.status == LicenceStatus.CANCELLED: + return + self.id = "27b79b32-1ce8-45a3-b7eb-18947bed2fcb" # PS-IGNORE + self.end_date = datetime.datetime.now().date() + super(Licence, self).save(*args, **kwargs) + + mocker.patch.object(Licence, "save", mock_licence_save_reissue) + + def mock_licence_decision_reissue(self, *args, **kwargs): + self.id = "5c821bf0-a60a-43ec-b4a0-2280f40f9995" # /PS-IGNORE + super(LicenceDecision, self).save(*args, **kwargs) + + mocker.patch.object(LicenceDecision, "save", mock_licence_decision_reissue) + + with freeze_time(timestamp): + issued_application.advice.filter(level=AdviceLevel.FINAL).update( + type=AdviceType.APPROVE, + text="reissuing the licence", + ) + + caseworker_change_status(issued_application, CaseStatusEnum.REOPENED_FOR_CHANGES) + caseworker_change_status(issued_application, CaseStatusEnum.UNDER_FINAL_REVIEW) + issue_licence(issued_application) diff --git a/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature b/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature index 0082082de0..b20ec219f3 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature @@ -1,20 +1,31 @@ @db -Feature: Destinations +Feature: destinations Table + + +Scenario: Draft application + Given a draft standard application + Then the `destinations` table is empty + Scenario: Check that the country code and type are included in the extract - Given a standard licence is created - When I fetch all destinations - Then the country code and type are included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + And a consignee added to the application in `Australia` + And an end-user added to the application of `New Zealand` + When the application is submitted + And the application is issued at 2024-11-22T13:35:15 + Then the `destinations` table has the following rows: + | application_id | country_code | type | + | 03fb08eb-1564-4b68-9336-3ca8906543f9 | NZ | end_user | + | 03fb08eb-1564-4b68-9336-3ca8906543f9 | AU | consignee | -Scenario: Deleted parties are not included in the extract - Given a licence with deleted party is created - When I fetch all destinations - Then the existing party is included in the extract - And the deleted party is not included in the extract -Scenario: Draft applications are not included in the extract - Given a standard licence is created - And a draft application is created - When I fetch all destinations - Then the non-draft party is included in the extract - And draft parties are not included in the extract +Scenario: Deleted parties are not included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted + And the application is issued at 2024-11-22T13:35:15 + And the parties are deleted + Then the `destinations` table is empty diff --git a/api/data_workspace/v2/tests/bdd/scenarios/goods.feature b/api/data_workspace/v2/tests/bdd/scenarios/goods.feature index 60137224e6..81ed76e3c9 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/goods.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/goods.feature @@ -1,14 +1,20 @@ @db -Feature: Goods +Feature: goods Table -Scenario: Check that the quantity, unit, value are included in the extract - Given a standard application is created - When I fetch all goods - Then the quantity, unit, value are included in the extract -Scenario: Draft applications are not included in the extract - Given a standard application is created - And a draft application is created - When I fetch all goods - Then the non-draft good is included in the extract - And the draft good is not included in the extract +Scenario: Draft application + Given a draft standard application + Then the `goods` table is empty + +@test +Scenario: Check that the quantity, unit, value are included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + And the application has the following goods: + | id | name | quantity | unit | value | + | 94590c78-d0a9-406d-8fd3-b913bf5867a9 | A controlled good | 100.00 | NAR | 1500.00 | + When the application is submitted + Then the `goods` table has the following rows: + | id | application_id | quantity | unit | value | + | 94590c78-d0a9-406d-8fd3-b913bf5867a9 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | 100.00 | NAR | 1500.00 | diff --git a/api/data_workspace/v2/tests/bdd/scenarios/licence_decisions.feature b/api/data_workspace/v2/tests/bdd/scenarios/licence_decisions.feature index e74e24677c..69a20e6aa7 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/licence_decisions.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/licence_decisions.feature @@ -1,5 +1,5 @@ @db -Feature: Licence Decisions +Feature: licence_decisions Table Scenario: Draft licence Given a standard draft licence is created @@ -9,88 +9,85 @@ Scenario: Cancelled licence Given a standard licence is cancelled Then the `licence_decisions` table is empty + # [ISSUED] Scenario: Issued licence decision is created when licence is issued - Given a case is ready to be finalised - When the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then a licence decision with an issued decision is created - When I fetch all licence decisions - Then I see issued licence is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is issued at 2024-11-22T13:35:15 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | ebd27511-7be3-4e5c-9ce9-872ad22811a1 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | issued | 2024-11-22T13:35:15 | 1b2f95c3-9cd2-4dee-b134-a79786f78c06 | + # [REFUSED] Scenario: Refused licence decision is created when licence is refused - Given a case is ready to be refused - When the licence for the case is refused - And case officer generates refusal documents - And case officer refuses licence for this case - Then a licence decision with refused decision is created - When I fetch all licence decisions - Then I see refused case is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is refused at 2024-11-22T13:35:15 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | 4ea4261f-03f2-4baf-8784-5ec4b352d358 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | refused | 2024-11-22T13:35:15 | None | + # [ISSUED, REVOKED] Scenario: Revoked licence decision is created when licence is revoked - Given a case is ready to be finalised - When the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then I see issued licence is included in the extract - When case officer revokes issued licence - And I fetch all licence decisions - Then I see revoked licence is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is issued at 2024-11-22T13:35:15 + And the issued application is revoked at 2024-11-25T14:22:09 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | ebd27511-7be3-4e5c-9ce9-872ad22811a1 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | issued | 2024-11-22T13:35:15 | 1b2f95c3-9cd2-4dee-b134-a79786f78c06 | + | 65ad0aa8-64ad-4805-92f1-86a4874e9fe6 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | revoked | 2024-11-25T14:22:09 | 1b2f95c3-9cd2-4dee-b134-a79786f78c06 | + # [REFUSED, ISSUED_ON_APPEAL] Scenario: Licence issued after an appeal is recorded as issued_on_appeal - Given a case is ready to be refused - When the licence for the case is refused - And case officer generates refusal documents - And case officer refuses licence for this case - When I fetch all licence decisions - Then I see refused case is included in the extract - When an appeal is successful and case is ready to be finalised - And the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then a licence decision with an issued_on_appeal decision is created - When I fetch all licence decisions - Then I see issued licence is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is refused at 2024-11-22T13:35:15 + And the application is appealed at 2024-11-25T14:22:09 + And the refused application is issued on appeal at 2024-11-29T10:20:09 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | 4ea4261f-03f2-4baf-8784-5ec4b352d358 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | refused | 2024-11-22T13:35:15 | None | + | f0bc0c1e-c9c5-4a90-b4c8-81a7f3cbe1e7 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | issued_on_appeal | 2024-11-29T10:20:09 | 4106ced1-b2b9-41e8-ad42-47c36b07b345 | + # [REFUSED, ISSUED_ON_APPEAL, ISSUED_ON_APPEAL] Scenario: Licence issued after an appeal and re-issued again - Given a case is ready to be refused - When the licence for the case is refused - And case officer generates refusal documents - And case officer refuses licence for this case - When I fetch all licence decisions - Then I see refused case is included in the extract - When an appeal is successful and case is ready to be finalised - And the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then a licence decision with an issued_on_appeal decision is created - When I fetch all licence decisions - Then I see issued licence is included in the extract - When a licence needs amending and case is ready to be finalised - And the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then a licence decision with an issued_on_appeal decision is created - When I fetch all licence decisions - Then I see issued licence is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is refused at 2024-11-22T13:35:15 + And the application is appealed at 2024-11-25T14:22:09 + And the refused application is issued on appeal at 2024-11-29T10:20:09 + And the application is reissued at 2024-12-29T10:20:09 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | 4ea4261f-03f2-4baf-8784-5ec4b352d358 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | refused | 2024-11-22T13:35:15 | None | + | f0bc0c1e-c9c5-4a90-b4c8-81a7f3cbe1e7 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | issued_on_appeal | 2024-11-29T10:20:09 | 27b79b32-1ce8-45a3-b7eb-18947bed2fcb | + -# [ISSUED, REFUSED] +# [ISSUED, REVOKED] Scenario: Licence is issued and refused case - Given a case is ready to be finalised - When the licence for the case is approved - And case officer generates licence documents - And case officer issues licence for this case - Then a licence decision with an issued decision is created - When I fetch all licence decisions - Then I see issued licence is included in the extract - When a licence needs refusing and case is ready to be finalised - And the licence for the case is refused - And case officer generates refusal documents - And case officer refuses licence for this case - When I fetch all licence decisions - Then I see refused case is included in the extract + Given a draft standard application with attributes: + | name | value | + | id | 03fb08eb-1564-4b68-9336-3ca8906543f9 | + When the application is submitted at 2024-10-01T11:20:15 + And the application is issued at 2024-11-22T13:35:15 + And the application is refused at 2024-11-22T13:35:15 + Then the `licence_decisions` table has the following rows: + | id | application_id | decision | decision_made_at | licence_id | + | ebd27511-7be3-4e5c-9ce9-872ad22811a1 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | issued | 2024-11-22T13:35:15 | 1b2f95c3-9cd2-4dee-b134-a79786f78c06 | + | 4ea4261f-03f2-4baf-8784-5ec4b352d358 | 03fb08eb-1564-4b68-9336-3ca8906543f9 | refused | 2024-11-22T13:35:15 | None | diff --git a/api/data_workspace/v2/tests/bdd/test_applications.py b/api/data_workspace/v2/tests/bdd/test_applications.py index cc02194aac..5f50e81a67 100644 --- a/api/data_workspace/v2/tests/bdd/test_applications.py +++ b/api/data_workspace/v2/tests/bdd/test_applications.py @@ -1,15 +1,7 @@ -import datetime - import pytest -import pytz from django.urls import reverse from freezegun import freeze_time -from pytest_bdd import ( - given, - parsers, - scenarios, - when, -) +from pytest_bdd import given, parsers, scenarios, when from api.applications.enums import ApplicationExportType from api.applications.tests.factories import ( @@ -17,30 +9,13 @@ GoodOnApplicationFactory, PartyOnApplicationFactory, ) -from api.cases.celery_tasks import update_cases_sla -from api.cases.enums import ( - AdviceLevel, - AdviceType, -) -from api.licences.enums import LicenceStatus -from api.parties.tests.factories import ( - PartyDocumentFactory, - UltimateEndUserFactory, -) +from api.data_workspace.v2.tests.bdd.conftest import run_processing_time_task +from api.parties.tests.factories import PartyDocumentFactory, UltimateEndUserFactory from api.staticdata.statuses.enums import CaseStatusEnum scenarios("./scenarios/applications.feature") -def run_processing_time_task(start, up_to): - processing_time_task_run_date_time = start.replace(hour=22, minute=30) - up_to = pytz.utc.localize(datetime.datetime.fromisoformat(up_to)) - while processing_time_task_run_date_time <= up_to: - with freeze_time(processing_time_task_run_date_time): - update_cases_sla() - processing_time_task_run_date_time = processing_time_task_run_date_time + datetime.timedelta(days=1) - - @pytest.fixture def submit_application(api_client, exporter_headers, mocker): def _submit_application(draft_application): @@ -69,27 +44,6 @@ def _submit_application(draft_application): return _submit_application -@pytest.fixture() -def caseworker_change_status(api_client, lu_case_officer, lu_case_officer_headers): - def _caseworker_change_status(application, status): - url = reverse( - "caseworker_applications:change_status", - kwargs={ - "pk": str(application.pk), - }, - ) - response = api_client.post( - url, - data={"status": status}, - **lu_case_officer_headers, - ) - assert response.status_code == 200, response.content - application.refresh_from_db() - assert application.status.status == status - - return _caseworker_change_status - - @pytest.fixture() def exporter_change_status(api_client, exporter_headers): def _exporter_change_status(application, status): @@ -166,108 +120,6 @@ def when_the_application_is_submitted_at(submit_application, draft_standard_appl return submit_application(draft_standard_application) -@when(parsers.parse("the application is issued at {timestamp}"), target_fixture="issued_application") -def when_the_application_is_issued_at( - issue_licence, - submitted_standard_application, - timestamp, -): - run_processing_time_task(submitted_standard_application.submitted_at, timestamp) - - with freeze_time(timestamp): - issue_licence(submitted_standard_application) - - submitted_standard_application.refresh_from_db() - issued_application = submitted_standard_application - - return issued_application - - -@when(parsers.parse("the application is refused at {timestamp}"), target_fixture="refused_application") -def when_the_application_is_refused_at( - submitted_standard_application, - refuse_application, - timestamp, -): - run_processing_time_task(submitted_standard_application.submitted_at, timestamp) - - with freeze_time(timestamp): - refuse_application(submitted_standard_application) - - submitted_standard_application.refresh_from_db() - refused_application = submitted_standard_application - - return refused_application - - -@when(parsers.parse("the application is appealed at {timestamp}"), target_fixture="appealed_application") -def when_the_application_is_appealed_at( - refused_application, - api_client, - exporter_headers, - timestamp, -): - with freeze_time(timestamp): - response = api_client.post( - reverse( - "applications:appeals", - kwargs={ - "pk": refused_application.pk, - }, - ), - data={ - "grounds_for_appeal": "This is appealing", - }, - **exporter_headers, - ) - assert response.status_code == 201, response.content - - refused_application.refresh_from_db() - appealed_application = refused_application - - return appealed_application - - -@when(parsers.parse("the refused application is issued on appeal at {timestamp}")) -def when_the_application_is_issued_on_appeal_at( - appealed_application, - timestamp, - caseworker_change_status, - issue_licence, -): - run_processing_time_task(appealed_application.appeal.created_at, timestamp) - - with freeze_time(timestamp): - appealed_application.advice.filter(level=AdviceLevel.FINAL).update( - type=AdviceType.APPROVE, - text="issued on appeal", - ) - - caseworker_change_status(appealed_application, CaseStatusEnum.REOPENED_FOR_CHANGES) - caseworker_change_status(appealed_application, CaseStatusEnum.UNDER_FINAL_REVIEW) - issue_licence(appealed_application) - - -@when(parsers.parse("the issued application is revoked at {timestamp}")) -def when_the_issued_application_is_revoked( - api_client, - lu_sr_manager_headers, - issued_application, - timestamp, -): - run_processing_time_task(issued_application.submitted_at, timestamp) - - with freeze_time(timestamp): - issued_licence = issued_application.licences.get() - url = reverse("licences:licence_details", kwargs={"pk": str(issued_licence.pk)}) - response = api_client.patch( - url, - data={"status": LicenceStatus.REVOKED}, - **lu_sr_manager_headers, - ) - assert response.status_code == 200, response.status_code - - @when(parsers.parse("the application is withdrawn at {timestamp}")) def when_the_application_is_withdrawn_at( submitted_standard_application, diff --git a/api/data_workspace/v2/tests/bdd/test_countries.py b/api/data_workspace/v2/tests/bdd/test_countries.py index add016403c..7b3935b99b 100644 --- a/api/data_workspace/v2/tests/bdd/test_countries.py +++ b/api/data_workspace/v2/tests/bdd/test_countries.py @@ -1,11 +1,6 @@ import pytest from django.urls import reverse -from pytest_bdd import ( - given, - scenarios, - then, - when, -) +from pytest_bdd import given, scenarios, then, when from api.staticdata.countries.models import Country diff --git a/api/data_workspace/v2/tests/bdd/test_destinations.py b/api/data_workspace/v2/tests/bdd/test_destinations.py index 8a591db03f..9a63f07725 100644 --- a/api/data_workspace/v2/tests/bdd/test_destinations.py +++ b/api/data_workspace/v2/tests/bdd/test_destinations.py @@ -1,129 +1,10 @@ -import pytest -from django.urls import reverse -from pytest_bdd import ( - given, - scenarios, - then, - when, -) +from datetime import datetime -from api.applications.models import PartyOnApplication -from api.licences.enums import LicenceStatus -from api.parties.enums import PartyType -from api.staticdata.statuses.enums import CaseStatusEnum +from pytest_bdd import scenarios, when scenarios("./scenarios/destinations.feature") -@pytest.fixture() -def destinations_list_url(): - return reverse("data_workspace:v2:dw-destinations-list") - - -@given("a standard licence is created", target_fixture="licence") -def standard_licence_created(standard_licence): - assert standard_licence.status == LicenceStatus.ISSUED - return standard_licence - - -@when("I fetch all destinations", target_fixture="destinations") -def fetch_all_destinations(destinations_list_url, unpage_data): - return unpage_data(destinations_list_url) - - -@then("the country code and type are included in the extract") -def country_code_and_type_included_in_extract(destinations): - party_on_application = PartyOnApplication.objects.get() - application_id = party_on_application.application_id - country_code = party_on_application.party.country.id - party_type = party_on_application.party.type - - destination = {"application_id": str(application_id), "country_code": country_code, "type": party_type} - assert destination in destinations - - -@given("a licence with deleted party is created", target_fixture="licence_with_deleted_party") -def licence_with_deleted_party_created(licence_with_deleted_party): - assert licence_with_deleted_party.status == LicenceStatus.ISSUED - application = licence_with_deleted_party.case.baseapplication - assert PartyOnApplication.objects.filter(application=application).count() == 2 - - -@then("the existing party is included in the extract") -def existing_party_included_in_extract(destinations): - existing_party_on_application = PartyOnApplication.objects.get(deleted_at__isnull=True) - application_id = existing_party_on_application.application_id - country_code = existing_party_on_application.party.country.id - party_type = existing_party_on_application.party.type - - assert PartyOnApplication.objects.filter(application_id=application_id).count() == 2 - - destination = {"application_id": str(application_id), "country_code": country_code, "type": party_type} - assert destination in destinations - assert len(destinations) == 1 - - -@then("the deleted party is not included in the extract") -def deleted_party_not_included_in_extract(destinations): - deleted_party_on_application = PartyOnApplication.objects.get(deleted_at__isnull=False) - application_id = deleted_party_on_application.application_id - country_code = deleted_party_on_application.party.country.id - party_type = deleted_party_on_application.party.type - - assert PartyOnApplication.objects.filter(application_id=application_id).count() == 2 - - destination = {"application_id": str(application_id), "country_code": country_code, "type": party_type} - assert destination not in destinations - assert len(destinations) == 1 - - -@given("a draft application is created") -def draft_application_created(draft_application): - assert draft_application.status.status == CaseStatusEnum.DRAFT - return draft_application - - -@then("the non-draft party is included in the extract") -def non_draft_party_is_included_in_extract(destinations): - non_draft_party_on_application = PartyOnApplication.objects.get( - application__status__status=CaseStatusEnum.FINALISED - ) - application_id = non_draft_party_on_application.application_id - country_code = non_draft_party_on_application.party.country.id - party_type = non_draft_party_on_application.party.type - - destination = {"application_id": str(application_id), "country_code": country_code, "type": party_type} - assert destination in destinations - assert len(destinations) == 1 - - -@then("draft parties are not included in the extract") -def draft_parties_not_included_in_extract(destinations): - draft_parties_on_application = PartyOnApplication.objects.filter(application__status__status=CaseStatusEnum.DRAFT) - - application_id = draft_parties_on_application.first().application_id - - draft_end_user = draft_parties_on_application.get(party__type=PartyType.END_USER) - draft_end_user_country_code = draft_end_user.party.country.id - draft_end_user_party_type = draft_end_user.party.type - - assert PartyOnApplication.objects.filter(application_id=application_id).count() == 2 - - draft_end_user_destination = { - "application_id": str(application_id), - "country_code": draft_end_user_country_code, - "type": draft_end_user_party_type, - } - assert draft_end_user_destination not in destinations - - draft_consignee = draft_parties_on_application.get(party__type=PartyType.CONSIGNEE) - draft_consignee_country_code = draft_consignee.party.country.id - draft_consignee_party_type = draft_consignee.party.type - - draft_consignee_destination = { - "application_id": str(application_id), - "country_code": draft_consignee_country_code, - "party_type": draft_consignee_party_type, - } - assert draft_consignee_destination not in destinations - assert len(destinations) == 1 +@when("the parties are deleted") +def when_parties_are_deleted(issued_application): + issued_application.parties.update(deleted_at=datetime.now()) diff --git a/api/data_workspace/v2/tests/bdd/test_footnotes.py b/api/data_workspace/v2/tests/bdd/test_footnotes.py index 393da8a52d..200a2f3b8b 100644 --- a/api/data_workspace/v2/tests/bdd/test_footnotes.py +++ b/api/data_workspace/v2/tests/bdd/test_footnotes.py @@ -1,10 +1,6 @@ import pytest from django.urls import reverse -from pytest_bdd import ( - parsers, - scenarios, - when, -) +from pytest_bdd import parsers, scenarios, when from api.staticdata.statuses.enums import CaseStatusEnum from api.teams.models import Team diff --git a/api/data_workspace/v2/tests/bdd/test_goods.py b/api/data_workspace/v2/tests/bdd/test_goods.py index 1c34f90439..d52ee7af05 100644 --- a/api/data_workspace/v2/tests/bdd/test_goods.py +++ b/api/data_workspace/v2/tests/bdd/test_goods.py @@ -1,11 +1,6 @@ import pytest from django.urls import reverse -from pytest_bdd import ( - given, - scenarios, - then, - when, -) +from pytest_bdd import given, scenarios, then, when from api.applications.models import GoodOnApplication from api.staticdata.statuses.enums import CaseStatusEnum diff --git a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py index 013f7ff09b..3ce2804fb8 100644 --- a/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py +++ b/api/data_workspace/v2/tests/bdd/test_goods_on_licences.py @@ -1,18 +1,10 @@ from django.urls import reverse -from pytest_bdd import ( - given, - parsers, - scenarios, - when, -) +from pytest_bdd import given, parsers, scenarios, when from api.applications.tests.factories import GoodOnApplicationFactory from api.licences.enums import LicenceStatus from api.licences.tests.factories import StandardLicenceFactory -from api.staticdata.report_summaries.models import ( - ReportSummaryPrefix, - ReportSummarySubject, -) +from api.staticdata.report_summaries.models import ReportSummaryPrefix, ReportSummarySubject scenarios("./scenarios/goods_on_licences.feature") diff --git a/api/data_workspace/v2/tests/bdd/test_licence_decisions.py b/api/data_workspace/v2/tests/bdd/test_licence_decisions.py index cfaf3d01ab..55e24ced0d 100644 --- a/api/data_workspace/v2/tests/bdd/test_licence_decisions.py +++ b/api/data_workspace/v2/tests/bdd/test_licence_decisions.py @@ -1,320 +1,19 @@ -from unittest import mock +from pytest_bdd import given, scenarios -import pytest -from django.urls import reverse -from django.utils import timezone -from pytest_bdd import ( - given, - scenarios, - then, - when, -) - -from api.applications.models import StandardApplication -from api.cases.enums import ( - AdviceLevel, - AdviceType, - LicenceDecisionType, -) -from api.cases.models import LicenceDecision from api.licences.enums import LicenceStatus -from api.licences.models import Licence -from api.staticdata.statuses.enums import CaseStatusEnum -from api.staticdata.statuses.models import CaseStatus scenarios("./scenarios/licence_decisions.feature") -@pytest.fixture() -def licence_decisions_list_url(): - return reverse("data_workspace:v2:dw-licence-decisions-list") - - -@when("I fetch all licence decisions", target_fixture="licence_decisions") -def fetch_licence_decisions(licence_decisions_list_url, unpage_data): - return unpage_data(licence_decisions_list_url) - - @given("a standard draft licence is created", target_fixture="draft_licence") def standard_draft_licence_created(standard_draft_licence): assert standard_draft_licence.status == LicenceStatus.DRAFT return standard_draft_licence -@then("the draft licence is not included in the extract") -def draft_licence_not_included_in_extract(draft_licence, unpage_data, licence_decisions_list_url): - licences = unpage_data(licence_decisions_list_url) - - assert str(draft_licence.case.id) not in [item["application_id"] for item in licences] - - @given("a standard licence is cancelled", target_fixture="cancelled_licence") def standard_licence_is_cancelled(standard_licence): standard_licence.status = LicenceStatus.CANCELLED standard_licence.save() return standard_licence - - -@then("the cancelled licence is not included in the extract") -def cancelled_licence_not_included_in_extract(cancelled_licence, unpage_data, licence_decisions_list_url): - licences = unpage_data(licence_decisions_list_url) - - assert str(cancelled_licence.case.id) not in [item["application_id"] for item in licences] - - -@then("I see issued licence is included in the extract") -def licence_included_in_extract(issued_licence, unpage_data, licence_decisions_list_url): - licences = unpage_data(licence_decisions_list_url) - - assert str(issued_licence.case.id) in [item["application_id"] for item in licences] - - -@then("I see refused case is included in the extract") -def refused_case_included_in_extract(refused_case, unpage_data, licence_decisions_list_url): - licences = unpage_data(licence_decisions_list_url) - - assert str(refused_case.id) in [item["application_id"] for item in licences] - - -@given("a case is ready to be finalised", target_fixture="case_with_final_advice") -def case_ready_to_be_finalised(standard_case_with_final_advice): - assert standard_case_with_final_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.UNDER_FINAL_REVIEW) - return standard_case_with_final_advice - - -@given("a case is ready to be refused", target_fixture="case_with_refused_advice") -def case_ready_to_be_refused(standard_case_with_refused_advice): - assert standard_case_with_refused_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.UNDER_FINAL_REVIEW) - return standard_case_with_refused_advice - - -@when("the licence for the case is approved") -def licence_for_case_is_approved(client, gov_headers, case_with_final_advice): - application = StandardApplication.objects.get(id=case_with_final_advice.id) - data = {"action": AdviceType.APPROVE, "duration": 24} - for good_on_app in application.goods.all(): - data[f"quantity-{good_on_app.id}"] = str(good_on_app.quantity) - data[f"value-{good_on_app.id}"] = str(good_on_app.value) - - issue_date = timezone.now() - data.update({"year": issue_date.year, "month": issue_date.month, "day": issue_date.day}) - - url = reverse("applications:finalise", kwargs={"pk": case_with_final_advice.id}) - response = client.put(url, data, content_type="application/json", **gov_headers) - assert response.status_code == 200 - response = response.json() - - assert response["reference_code"] is not None - licence = Licence.objects.get(reference_code=response["reference_code"]) - assert licence.status == LicenceStatus.DRAFT - - -@when("case officer generates licence documents") -def case_officer_generates_licence_documents(client, siel_template, gov_headers, case_with_final_advice): - data = { - "template": str(siel_template.id), - "text": "", - "visible_to_exporter": False, - "advice_type": AdviceType.APPROVE, - } - url = reverse( - "cases:generated_documents:generated_documents", - kwargs={"pk": str(case_with_final_advice.pk)}, - ) - with mock.patch("api.cases.generated_documents.views.s3_operations.upload_bytes_file", return_value=None): - response = client.post(url, data, content_type="application/json", **gov_headers) - assert response.status_code == 201 - - -@when("case officer issues licence for this case", target_fixture="issued_licence") -def case_officer_issues_licence(client, gov_headers, case_with_final_advice): - url = reverse( - "cases:finalise", - kwargs={"pk": str(case_with_final_advice.pk)}, - ) - response = client.put(url, {}, content_type="application/json", **gov_headers) - assert response.status_code == 201 - - case_with_final_advice.refresh_from_db() - assert case_with_final_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) - assert case_with_final_advice.sub_status.name == "Approved" - - response = response.json() - assert response["licence"] is not None - - licence = Licence.objects.get(id=response["licence"]) - assert licence.status in [LicenceStatus.ISSUED, LicenceStatus.REINSTATED] - - return licence - - -@then("a licence decision with an issued decision is created") -def licence_decision_issued_created(issued_licence): - assert LicenceDecision.objects.filter( - case=issued_licence.case, - decision=LicenceDecisionType.ISSUED, - ).exists() - - -@when("the licence for the case is refused") -def licence_for_case_is_refused(client, gov_headers, case_with_refused_advice): - data = {"action": AdviceType.REFUSE} - - url = reverse("applications:finalise", kwargs={"pk": case_with_refused_advice.id}) - response = client.put(url, data, content_type="application/json", **gov_headers) - assert response.status_code == 200 - - -@when("case officer generates refusal documents") -def generate_refusal_documents(client, siel_refusal_template, gov_headers, case_with_refused_advice): - data = { - "template": str(siel_refusal_template.id), - "text": "", - "visible_to_exporter": False, - "advice_type": AdviceType.REFUSE, - } - url = reverse( - "cases:generated_documents:generated_documents", - kwargs={"pk": str(case_with_refused_advice.pk)}, - ) - with mock.patch("api.cases.generated_documents.views.s3_operations.upload_bytes_file", return_value=None): - response = client.post(url, data, content_type="application/json", **gov_headers) - assert response.status_code == 201 - - -@when("case officer refuses licence for this case", target_fixture="refused_case") -def case_officer_refuses_licence(client, gov_headers, case_with_refused_advice): - url = reverse( - "cases:finalise", - kwargs={"pk": str(case_with_refused_advice.pk)}, - ) - response = client.put(url, {}, content_type="application/json", **gov_headers) - assert response.status_code == 201, response.content - - case_with_refused_advice.refresh_from_db() - assert case_with_refused_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) - assert case_with_refused_advice.sub_status.name == "Refused" - - assert LicenceDecision.objects.filter( - case=case_with_refused_advice, - decision=LicenceDecisionType.REFUSED, - ).exists() - - return case_with_refused_advice - - -@then("a licence decision with refused decision is created") -def licence_decision_refused_created(refused_case): - assert LicenceDecision.objects.filter( - case=refused_case, - decision=LicenceDecisionType.REFUSED, - ).exists() - - -@when("case officer revokes issued licence", target_fixture="revoked_licence") -def case_officer_revokes_licence(client, lu_sr_manager_headers, issued_licence): - url = reverse("licences:licence_details", kwargs={"pk": str(issued_licence.pk)}) - response = client.patch( - url, {"status": LicenceStatus.REVOKED}, content_type="application/json", **lu_sr_manager_headers - ) - assert response.status_code == 200 - - assert LicenceDecision.objects.filter( - case=issued_licence.case, - decision=LicenceDecisionType.REVOKED, - ).exists() - - revoked_licence = LicenceDecision.objects.get( - case=issued_licence.case, decision=LicenceDecisionType.REVOKED - ).licence - - return revoked_licence - - -@then("I see revoked licence is included in the extract") -def revoked_licence_decision_included_in_extract(licence_decisions, revoked_licence): - - all_revoked_licences = [item for item in licence_decisions if item["decision"] == "revoked"] - - assert str(revoked_licence.case.id) in [item["application_id"] for item in all_revoked_licences] - - -def case_reopen_prepare_to_finalise(client, lu_case_officer_headers, case): - url = reverse( - "caseworker_applications:change_status", - kwargs={ - "pk": str(case.pk), - }, - ) - response = client.post( - url, {"status": CaseStatusEnum.REOPENED_FOR_CHANGES}, content_type="application/json", **lu_case_officer_headers - ) - assert response.status_code == 200 - case.refresh_from_db() - assert case.status == CaseStatus.objects.get(status=CaseStatusEnum.REOPENED_FOR_CHANGES) - - response = client.post( - url, {"status": CaseStatusEnum.UNDER_FINAL_REVIEW}, content_type="application/json", **lu_case_officer_headers - ) - assert response.status_code == 200 - case.refresh_from_db() - assert case.status == CaseStatus.objects.get(status=CaseStatusEnum.UNDER_FINAL_REVIEW) - - return case - - -@when("an appeal is successful and case is ready to be finalised", target_fixture="case_with_final_advice") -def case_ready_to_be_finalised_after_an_appeal(client, lu_case_officer_headers, refused_case): - # Appeal handling is a manual process and we need to remove previous final advice - # before the case can be finalised again - assert refused_case.status.status == CaseStatusEnum.FINALISED - - refused_case.advice.filter(level=AdviceLevel.FINAL).update( - type=AdviceType.APPROVE, - text="issued on appeal", - ) - - successful_appeal_case = case_reopen_prepare_to_finalise(client, lu_case_officer_headers, refused_case) - - return successful_appeal_case - - -@when("a licence needs amending and case is ready to be finalised", target_fixture="case_with_final_advice") -def case_ready_to_be_finalised_after_amending_licence(client, lu_case_officer_headers, issued_licence): - case_with_final_advice = issued_licence.case - assert case_with_final_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) - - case_with_final_advice.advice.filter(level=AdviceLevel.FINAL).update( - type=AdviceType.APPROVE, - text="re-issuing licence", - ) - - case_with_final_advice = case_reopen_prepare_to_finalise(client, lu_case_officer_headers, case_with_final_advice) - - return case_with_final_advice - - -@when("a licence needs refusing and case is ready to be finalised", target_fixture="case_with_refused_advice") -def case_ready_to_be_finalised_after_refusing_licence(client, lu_case_officer_headers, issued_licence): - case_with_final_advice = issued_licence.case - assert case_with_final_advice.status == CaseStatus.objects.get(status=CaseStatusEnum.FINALISED) - - final_advice = case_with_final_advice.advice.filter(level=AdviceLevel.FINAL) - for advice in final_advice: - advice.type = AdviceType.REFUSE - advice.text = "refusing licence" - advice.denial_reasons.set(["1a", "1b", "1c"]) - advice.save() - - case_with_final_advice = case_reopen_prepare_to_finalise(client, lu_case_officer_headers, case_with_final_advice) - - return case_with_final_advice - - -@then("a licence decision with an issued_on_appeal decision is created") -def licence_decision_issued_on_appeal_created(issued_licence): - all_licence_decisions = LicenceDecision.objects.filter(case=issued_licence.case) - - assert all_licence_decisions.first().decision == LicenceDecisionType.REFUSED - assert all(item.decision == LicenceDecisionType.ISSUED_ON_APPEAL for item in all_licence_decisions[1:]) diff --git a/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py b/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py index 6637981aa1..b8b55b5182 100644 --- a/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py +++ b/api/data_workspace/v2/tests/bdd/test_licence_refusal_criteria.py @@ -1,11 +1,6 @@ import uuid -from pytest_bdd import ( - parsers, - scenarios, - when, -) - +from pytest_bdd import parsers, scenarios, when scenarios("./scenarios/licence_refusal_criteria.feature") diff --git a/api/staticdata/report_summaries/migrations/0010_add_ars_prefix_dec_2024.py b/api/staticdata/report_summaries/migrations/0010_add_ars_prefix_dec_2024.py new file mode 100644 index 0000000000..0a523c6b5c --- /dev/null +++ b/api/staticdata/report_summaries/migrations/0010_add_ars_prefix_dec_2024.py @@ -0,0 +1,19 @@ +import json + +from django.db import migrations + +DATA_PATH = "api/staticdata/report_summaries/migrations/data/0010_add_ars_prefix_dec_2024/" + + +def populate_report_summaries(apps, schema_editor): + + ReportSummaryPrefix = apps.get_model("report_summaries", "ReportSummaryPrefix") + with open(f"{DATA_PATH}/report_summary_prefix.json") as json_file: + records = json.load(json_file) + for attributes in records: + ReportSummaryPrefix.objects.create(**attributes) + + +class Migration(migrations.Migration): + dependencies = [("report_summaries", "0009_add_ars_subject_prefix_oct_2024")] + operations = [migrations.RunPython(populate_report_summaries, migrations.RunPython.noop)] diff --git a/api/staticdata/report_summaries/migrations/data/0010_add_ars_prefix_dec_2024/report_summary_prefix.json b/api/staticdata/report_summaries/migrations/data/0010_add_ars_prefix_dec_2024/report_summary_prefix.json new file mode 100644 index 0000000000..7510a0e9bf --- /dev/null +++ b/api/staticdata/report_summaries/migrations/data/0010_add_ars_prefix_dec_2024/report_summary_prefix.json @@ -0,0 +1,110 @@ +[ + { + "id": "7b9e40d5-daa0-4936-a396-999f102802db", + "name": "components for training" + }, + { + "id": "d9c63989-f282-4bca-9f41-7c47a9b5bb45", + "name": "technology for equipment for the production of" + }, + { + "id": "b90c0700-239f-400a-8993-8b848fa0eb40", + "name": "technology for accessories for" + }, + { + "id": "0f9a0f6f-f2aa-474c-b0ce-bcd131280763", + "name": "technology for components for" + }, + { + "id": "00ed0089-b001-42b8-8269-a80c9fe6f5e6", + "name": "technology for production facilities for" + }, + { + "id": "d170137d-0e8e-49af-8e2e-4823cb30d141", + "name": "technology for equipment for the use of" + }, + { + "id": "12fb818d-5f45-46eb-9b27-c19e7879902d", + "name": "technology for equipment for the development of" + }, + { + "id": "750c60cf-0851-420f-85d8-ed5c93f11f2a", + "name": "technology for software for" + }, + { + "id": "c9cb488c-e389-49df-8cd6-35b67970185b", + "name": "software for accessories for" + }, + { + "id": "ea187231-3e41-4a00-82f3-0146011fd65d", + "name": "software for components for" + }, + { + "id": "b3693806-e775-46a2-b9af-13c62f8faf27", + "name": "software for production facilities for" + }, + { + "id": "b2d86c51-5ee1-47f6-9a74-8713f25b7303", + "name": "software for equipment for the use of" + }, + { + "id": "4373571f-d0a0-49bc-9ae5-4c398ad21a18", + "name": "software for equipment for the production of" + }, + { + "id": "cdce636f-065b-4dfa-b6c1-a666793b2195", + "name": "software for equipment for the development of" + }, + { + "id": "970126b6-e735-47cb-8401-fc31199bcbdf", + "name": "software for technology for" + }, + { + "id": "c616aa24-21da-44ed-8ebe-6da5e214ccdd", + "name": "technology for software for accessories for" + }, + { + "id": "ea40d261-c059-4e40-a592-2bc05673937c", + "name": "technology for software for components for" + }, + { + "id": "b8889393-4b5d-4be7-9cd8-bef21251758c", + "name": "technology for software for production facilities for" + }, + { + "id": "cf1d8f29-f84b-48f7-9b1a-e575ad499f65", + "name": "technology for software for equipment for the use of" + }, + { + "id": "20ecc6b0-1200-4e79-91a9-c8ae878b46ae", + "name": "technology for software for equipment for the production of" + }, + { + "id": "d1ffdbcb-313b-4dae-9052-1d92f8124d52", + "name": "technology for software for equipment for the development of" + }, + { + "id": "ebc0f475-458c-4124-b835-8a6448431901", + "name": "software for technology for accessories for" + }, + { + "id": "b53aac5b-5247-4576-ae6b-9b57c27974f4", + "name": "software for technology for components for" + }, + { + "id": "b3d2ca48-f51f-4737-b03c-425618fb4872", + "name": "software for technology for production facilities for" + }, + { + "id": "6571c253-80f1-465e-91d2-78bdbacc8381", + "name": "software for technology for equipment for the use of" + }, + { + "id": "7f7f1b7d-7c1e-4418-ba31-cf1267719d74", + "name": "software for technology for equipment for the production of" + }, + { + "id": "3bfa5e70-7d64-4972-9696-8b60d0506b29", + "name": "software for technology for equipment for the development of" + } +] diff --git a/api/staticdata/report_summaries/migrations/tests/test_0010_add_ars_prefix_dec_2024.py b/api/staticdata/report_summaries/migrations/tests/test_0010_add_ars_prefix_dec_2024.py new file mode 100644 index 0000000000..7a488b4f8a --- /dev/null +++ b/api/staticdata/report_summaries/migrations/tests/test_0010_add_ars_prefix_dec_2024.py @@ -0,0 +1,27 @@ +import json +import pytest + +FIXTURE_BASE = "api/staticdata/report_summaries/migrations/data/0010_add_ars_prefix_dec_2024/" +INITIAL_MIGRATION = "0009_add_ars_subject_prefix_oct_2024" +MIGRATION_UNDER_TEST = "0010_add_ars_prefix_dec_2024" + + +@pytest.mark.django_db() +def test_add_ars_prefix_dec(migrator): + with open(FIXTURE_BASE + "report_summary_prefix.json") as prefix_json_file: + report_summary_prefix_data = json.load(prefix_json_file) + + old_state = migrator.apply_initial_migration(("report_summaries", INITIAL_MIGRATION)) + ReportSummaryPrefix = old_state.apps.get_model("report_summaries", "ReportSummaryPrefix") + + for prefix_to_add in report_summary_prefix_data: + assert not ReportSummaryPrefix.objects.filter(name=prefix_to_add["name"]).exists() + assert not ReportSummaryPrefix.objects.filter(id=prefix_to_add["id"]).exists() + + new_state = migrator.apply_tested_migration(("report_summaries", MIGRATION_UNDER_TEST)) + + ReportSummaryPrefix = new_state.apps.get_model("report_summaries", "ReportSummaryPrefix") + + for expected_prefix in report_summary_prefix_data: + prefix = ReportSummaryPrefix.objects.get(name=expected_prefix["name"]) + assert str(prefix.id) == expected_prefix["id"] diff --git a/api/staticdata/report_summaries/tests/test_views.py b/api/staticdata/report_summaries/tests/test_views.py index 734d3165c6..4db1c5299d 100644 --- a/api/staticdata/report_summaries/tests/test_views.py +++ b/api/staticdata/report_summaries/tests/test_views.py @@ -54,6 +54,18 @@ def test_get_report_summary_prefixes_OK(self): "launching/handling/control/support equipment for", "oil and gas industry equipment/materials", "software enabling equipment to function as", + "software for equipment for the development of", + "software for equipment for the production of", + "software for equipment for the use of", + "software for technology for equipment for the development of", + "software for technology for equipment for the production of", + "software for technology for equipment for the use of", + "technology for equipment for the development of", + "technology for equipment for the production of", + "technology for equipment for the use of", + "technology for software for equipment for the development of", + "technology for software for equipment for the production of", + "technology for software for equipment for the use of", "test equipment for", "training equipment for", ], @@ -69,13 +81,13 @@ def test_get_report_summary_prefixes_OK(self): ) def test_get_report_summary_prefixes_with_name_filter(self, name, filter, expected_results): url = prefixes_url(filter) + response = self.client.get(url, **self.gov_headers) self.assertEqual(response.status_code, 200) prefixes = [prefix["name"] for prefix in response.json()["report_summary_prefixes"]] self.assertEqual(len(prefixes), len(expected_results)) - self.assertEqual(prefixes, expected_results)