From d200e8ba351a8920ecc731854d0723ee62823e9b Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 11 Dec 2024 13:20:50 +0000 Subject: [PATCH 1/6] Update validation code to check for multiple end-users --- api/applications/creators.py | 27 ++++++++++++++++----------- api/applications/models.py | 7 +++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/api/applications/creators.py b/api/applications/creators.py index 89930f2519..327eceb626 100644 --- a/api/applications/creators.py +++ b/api/applications/creators.py @@ -96,23 +96,28 @@ def check_party_error(party, object_not_found_error, is_mandatory, is_document_m return document_error -def _validate_end_user(draft, errors, is_mandatory, open_application=False): +def _validate_end_users(draft, errors, is_mandatory, open_application=False): """Validates end user. If a document is mandatory, this is also validated.""" # Document is only mandatory if application is standard permanent or HMRC query is_document_mandatory = ( draft.case_type.sub_type == CaseTypeSubTypeEnum.STANDARD and draft.export_type == ApplicationExportType.PERMANENT - ) or draft.case_type.sub_type == CaseTypeSubTypeEnum.HMRC - - end_user_errors = check_party_error( - draft.end_user.party if draft.end_user else None, - object_not_found_error=strings.Applications.Standard.NO_END_USER_SET, - is_mandatory=is_mandatory, - is_document_mandatory=is_document_mandatory, ) - if end_user_errors: - errors["end_user"] = [end_user_errors] + error_messages = [] + + for end_user in draft.end_users: + end_user_errors = check_party_error( + end_user.party if end_user else None, + object_not_found_error=strings.Applications.Standard.NO_END_USER_SET, + is_mandatory=is_mandatory, + is_document_mandatory=is_document_mandatory, + ) + if end_user_errors: + error_messages.append(end_user_errors) + + if error_messages: + errors["end_user"] = error_messages return errors @@ -263,7 +268,7 @@ def _validate_standard_licence(draft, errors): """Checks that a standard licence has all party types & goods""" errors = _validate_siel_locations(draft, errors) - errors = _validate_end_user(draft, errors, is_mandatory=True) + errors = _validate_end_users(draft, errors, is_mandatory=True) errors = _validate_security_approvals(draft, errors, is_mandatory=True) errors = _validate_consignee(draft, errors, is_mandatory=True) errors = _validate_third_parties(draft, errors, is_mandatory=False) diff --git a/api/applications/models.py b/api/applications/models.py index 4610d6f472..511097fa7f 100644 --- a/api/applications/models.py +++ b/api/applications/models.py @@ -155,6 +155,13 @@ def end_user(self): except PartyOnApplication.DoesNotExist: pass + @property + def end_users(self): + try: + return self.active_parties.filter(party__type=PartyType.END_USER) + except PartyOnApplication.DoesNotExist: + pass + @property def ultimate_end_users(self): """ From c8d5f4bbc919973b6c8a6e0954c30d03d61d10e1 Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 11 Dec 2024 16:12:35 +0000 Subject: [PATCH 2/6] Update context generator to handle multiple end-users We still only display one end-user which will be updated later. --- api/letter_templates/context_generator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/api/letter_templates/context_generator.py b/api/letter_templates/context_generator.py index 7db1fdc574..f648980015 100644 --- a/api/letter_templates/context_generator.py +++ b/api/letter_templates/context_generator.py @@ -737,6 +737,7 @@ def get_document_context(case, addressee=None): appeal_deadline = timezone.localtime() + timedelta(days=APPEAL_DAYS) exporter_reference = "" date_application_submitted = "" + end_users = [] if base_application: if base_application.name: @@ -745,6 +746,8 @@ def get_document_context(case, addressee=None): if base_application.submitted_at: date_application_submitted = base_application.submitted_at.strftime("%d %B %Y") + end_users = base_application.end_users + return { "case_reference": case.reference_code, "case_submitted_at": case.submitted_at, @@ -756,11 +759,7 @@ def get_document_context(case, addressee=None): "addressee": AddresseeSerializer(addressee).data, "organisation": OrganisationSerializer(case.organisation).data, "licence": LicenceSerializer(licence).data if licence else None, - "end_user": ( - PartySerializer(base_application.end_user.party).data - if base_application and base_application.end_user - else None - ), + "end_user": (PartySerializer(end_users[0].party).data if end_users else None), "consignee": ( PartySerializer(base_application.consignee.party).data if base_application and base_application.consignee From 30ead380700b2dd1bf1a7798a5dfb5b7934dd95d Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 11 Dec 2024 16:22:48 +0000 Subject: [PATCH 3/6] Match sanctions on the first end-user --- api/applications/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/applications/helpers.py b/api/applications/helpers.py index d9f70c4a8b..0b5717e929 100644 --- a/api/applications/helpers.py +++ b/api/applications/helpers.py @@ -161,8 +161,8 @@ def delete_uploaded_document(data): def auto_match_sanctions(application): parties = [] - if application.end_user: - parties.append(application.end_user.party) + if application.end_users[0]: + parties.append(application.end_users[0].party) for item in application.ultimate_end_users: parties.append(item.party) From a0dd69aeba5569532213fd33022ef378975076ed Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 11 Dec 2024 16:32:16 +0000 Subject: [PATCH 4/6] Use first end_user if there are multiple end_users --- api/applications/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/applications/models.py b/api/applications/models.py index 511097fa7f..df3820e6a5 100644 --- a/api/applications/models.py +++ b/api/applications/models.py @@ -151,7 +151,7 @@ def end_user(self): Standard and HMRC Query applications """ try: - return self.active_parties.get(party__type=PartyType.END_USER) + return self.active_parties.filter(party__type=PartyType.END_USER).first() except PartyOnApplication.DoesNotExist: pass From 3826af7ee02df78a9f8cf08102cb4616c7437e22 Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Tue, 10 Dec 2024 09:40:24 +0000 Subject: [PATCH 5/6] Fix sorting issue Current code sorting the results based on first key in the results but for one of the test this is same for all rows so randomly it sorts by different order and causing the test to fail. Update the test to sort by all keys so we have a consistent order. --- api/data_workspace/v2/tests/bdd/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index e052c8471c..4bf5c3e915 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -9,6 +9,7 @@ from django.urls import reverse from freezegun import freeze_time from moto import mock_aws +from operator import itemgetter from pytest_bdd import ( given, parsers, @@ -522,8 +523,8 @@ def check_rows(client, parse_table, unpage_data, table_name, rows): 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 item, keys=keys: item[keys[0]]) - expected_data = sorted(expected_data, key=lambda item, keys=keys: item[keys[0]]) + actual_data = sorted(actual_data, key=itemgetter(*keys)) + expected_data = sorted(expected_data, key=itemgetter(*keys)) assert actual_data == expected_data From 0be867df6f2e3459d535e6005403e777d1cc8e6d Mon Sep 17 00:00:00 2001 From: Arun Siluvery Date: Wed, 11 Dec 2024 16:39:26 +0000 Subject: [PATCH 6/6] Update destinations bdd test to have multiple end_users --- api/data_workspace/v2/tests/bdd/conftest.py | 15 +++++++++++++++ .../v2/tests/bdd/scenarios/destinations.feature | 2 ++ 2 files changed, 17 insertions(+) diff --git a/api/data_workspace/v2/tests/bdd/conftest.py b/api/data_workspace/v2/tests/bdd/conftest.py index 4bf5c3e915..2e2c8f9c4e 100644 --- a/api/data_workspace/v2/tests/bdd/conftest.py +++ b/api/data_workspace/v2/tests/bdd/conftest.py @@ -24,6 +24,7 @@ from api.applications.tests.factories import ( DraftStandardApplicationFactory, GoodOnApplicationFactory, + EndUserFactory, PartyOnApplicationFactory, StandardApplicationFactory, ) @@ -440,6 +441,20 @@ def add_end_user_to_application(draft_standard_application, country): end_user.party.save() +@given(parsers.parse("a new end-user added to the application of `{country}`")) +def add_new_end_user_to_application(draft_standard_application, country): + country = Country.objects.get(name=country) + end_user = PartyOnApplicationFactory( + application=draft_standard_application, + party=EndUserFactory(country=country, organisation=draft_standard_application.organisation), + ) + PartyDocumentFactory( + party=end_user.party, + s3_key="party-document-second", + safe=True, + ) + + @when( "the application is submitted", target_fixture="submitted_standard_application", diff --git a/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature b/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature index b20ec219f3..f8bf4e0773 100644 --- a/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature +++ b/api/data_workspace/v2/tests/bdd/scenarios/destinations.feature @@ -13,10 +13,12 @@ Scenario: Check that the country code and type are included in the extract | 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` + And a new end-user added to the application of `South Korea` 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 | KR | end_user | | 03fb08eb-1564-4b68-9336-3ca8906543f9 | NZ | end_user | | 03fb08eb-1564-4b68-9336-3ca8906543f9 | AU | consignee |