From 98187688d6a126e34f2efb8d0c2d7fe9208b439e Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 26 Feb 2025 14:25:21 +0100 Subject: [PATCH] [#267] Improvements --- .../klantinteracties/admin/partijen.py | 12 +- .../api/serializers/partijen.py | 42 +- .../api/tests/test_partijen.py | 380 ++++++++++-------- .../klantinteracties/api/viewsets/partijen.py | 5 + ...rtijidentificator_sub_identificator_van.py | 34 -- ...ificator_sub_identificator_van_and_more.py | 67 +++ .../klantinteracties/models/partijen.py | 75 +++- .../models/tests/test_partijen.py | 238 +++++++++++ .../klantinteracties/models/validators.py | 132 +----- .../components/klantinteracties/openapi.yaml | 15 + .../klantinteracties/tests/test_validators.py | 177 +------- src/openklant/utils/decorators.py | 23 ++ 12 files changed, 680 insertions(+), 520 deletions(-) delete mode 100644 src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van.py create mode 100644 src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van_and_more.py create mode 100644 src/openklant/components/klantinteracties/models/tests/test_partijen.py create mode 100644 src/openklant/utils/decorators.py diff --git a/src/openklant/components/klantinteracties/admin/partijen.py b/src/openklant/components/klantinteracties/admin/partijen.py index 23c4ad7d..aa8d7e66 100644 --- a/src/openklant/components/klantinteracties/admin/partijen.py +++ b/src/openklant/components/klantinteracties/admin/partijen.py @@ -39,19 +39,11 @@ def clean(self): "code_register": cleaned_data["partij_identificator_code_register"], } - PartijIdentificatorTypesValidator( - partij_identificator=partij_identificator - ).validate() - - queryset = PartijIdentificator.objects.exclude() - if self.instance: - queryset = queryset.exclude(pk=self.instance.pk) + PartijIdentificatorTypesValidator()(partij_identificator) PartijIdentificatorUniquenessValidator( - instance=self.instance, - partij_identificator=partij_identificator, + code_soort_object_id=partij_identificator["code_soort_object_id"], sub_identificator_van=cleaned_data["sub_identificator_van"], - partij=cleaned_data["partij"], )() return cleaned_data diff --git a/src/openklant/components/klantinteracties/api/serializers/partijen.py b/src/openklant/components/klantinteracties/api/serializers/partijen.py index 5bf788dc..cc2e39aa 100644 --- a/src/openklant/components/klantinteracties/api/serializers/partijen.py +++ b/src/openklant/components/klantinteracties/api/serializers/partijen.py @@ -46,6 +46,7 @@ PartijIdentificatorTypesValidator, PartijIdentificatorUniquenessValidator, ) +from openklant.utils.decorators import handle_db_exceptions from openklant.utils.serializers import get_field_instance_by_uuid, get_field_value @@ -371,10 +372,7 @@ class Meta: "code_soort_object_id": {"required": True}, "object_id": {"required": True}, } - - def validate(self, attrs): - PartijIdentificatorTypesValidator(partij_identificator=attrs)() - return super().validate(attrs) + validators = [] class PartijIdentificatorSerializer( @@ -420,26 +418,40 @@ class Meta: } def validate(self, attrs): - instance = getattr(self, "instance", None) partij_identificator = get_field_value(self, attrs, "partij_identificator") - sub_identificator_van = get_field_instance_by_uuid( self, attrs, "sub_identificator_van", PartijIdentificator ) - partij = get_field_instance_by_uuid(self, attrs, "partij", Partij) - + PartijIdentificatorTypesValidator()(partij_identificator) PartijIdentificatorUniquenessValidator( - instance=instance, - partij_identificator=partij_identificator, + code_soort_object_id=partij_identificator["code_soort_object_id"], sub_identificator_van=sub_identificator_van, - partij=partij, )() + return super().validate(attrs) - attrs["sub_identificator_van"] = get_field_instance_by_uuid( - self, attrs, "sub_identificator_van", PartijIdentificator + @handle_db_exceptions + @transaction.atomic + def update(self, instance, validated_data): + validated_data["sub_identificator_van"] = get_field_instance_by_uuid( + self, validated_data, "sub_identificator_van", PartijIdentificator ) - attrs["partij"] = get_field_instance_by_uuid(self, attrs, "partij", Partij) - return super().validate(attrs) + validated_data["partij"] = get_field_instance_by_uuid( + self, validated_data, "partij", Partij + ) + + return super().update(instance, validated_data) + + @handle_db_exceptions + @transaction.atomic + def create(self, validated_data): + validated_data["sub_identificator_van"] = get_field_instance_by_uuid( + self, validated_data, "sub_identificator_van", PartijIdentificator + ) + validated_data["partij"] = get_field_instance_by_uuid( + self, validated_data, "partij", Partij + ) + + return super().create(validated_data) class PartijSerializer(NestedGegevensGroepMixin, PolymorphicSerializer): diff --git a/src/openklant/components/klantinteracties/api/tests/test_partijen.py b/src/openklant/components/klantinteracties/api/tests/test_partijen.py index b2082dcf..efffc765 100644 --- a/src/openklant/components/klantinteracties/api/tests/test_partijen.py +++ b/src/openklant/components/klantinteracties/api/tests/test_partijen.py @@ -1809,13 +1809,16 @@ def _get_detail_url(partij: Partij) -> str: class PartijIdentificatorTests(APITestCase): def test_list_partij_identificator(self): list_url = reverse("klantinteracties:partijidentificator-list") + partij = PartijFactory.create() PartijIdentificator.objects.create( + partij=partij, partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", partij_identificator_object_id="296648875", partij_identificator_code_register="brp", ) PartijIdentificator.objects.create( + partij=partij, partij_identificator_code_objecttype="niet_natuurlijk_persoon", partij_identificator_code_soort_object_id="rsin", partij_identificator_object_id="296648875", @@ -2034,9 +2037,7 @@ def test_invalid_validation_partij_identificator_code_objecttype(self): } response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors( - response, "partijIdentificator.partijIdentificatorCodeObjecttype" - ) + error = get_validation_errors(response, "partijIdentificatorCodeObjecttype") self.assertEqual(error["code"], "invalid") self.assertEqual( error["reason"], @@ -2059,9 +2060,7 @@ def test_invalid_validation_partij_identificator_code_soort_object_id(self): response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors( - response, "partijIdentificator.partijIdentificatorCodeSoortObjectId" - ) + error = get_validation_errors(response, "partijIdentificatorCodeSoortObjectId") self.assertEqual(error["code"], "invalid") self.assertEqual( @@ -2086,9 +2085,7 @@ def test_invalid_validation_partij_identificator_object_id(self): response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors( - response, "partijIdentificator.partijIdentificatorObjectId" - ) + error = get_validation_errors(response, "partijIdentificatorObjectId") self.assertEqual(error["code"], "invalid") self.assertEqual( error["reason"], @@ -2108,9 +2105,10 @@ def test_invalid_create_empty(self): response = self.client.post(list_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(len(response.data["invalid_params"]), 4) - error = get_validation_errors(response, "partijIdentificator.codeObjecttype") - self.assertEqual(error["code"], "required") - self.assertEqual(error["reason"], "Dit veld is vereist.") + get_validation_errors(response, "partijIdentificator.objectId") + get_validation_errors(response, "partijIdentificator.codeSoortObjectId") + get_validation_errors(response, "partijIdentificator.codeObjecttype") + get_validation_errors(response, "partijIdentificator.codeRegister") def test_invalid_create_partial(self): # all partij_identificator fields required @@ -2192,16 +2190,15 @@ def test_valid_create(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(PartijIdentificator.objects.all().count(), 1) - def test_valid_create_sub_identificator_van(self): + def test_valid_create_with_sub_identificator_van(self): partij_identificator = PartijIdentificatorFactory.create( partij=self.partij, andere_partij_identificator="anderePartijIdentificator", - partij_identificator_code_objecttype="natuurlijk_persoon", - partij_identificator_code_soort_object_id="bsn", - partij_identificator_object_id="123456782", - partij_identificator_code_register="brp", + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="12345678", + partij_identificator_code_register="hr", ) - data = { "identificeerdePartij": {"uuid": str(self.partij.uuid)}, "sub_identificator_van": {"uuid": str(partij_identificator.uuid)}, @@ -2216,13 +2213,25 @@ def test_valid_create_sub_identificator_van(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(PartijIdentificator.objects.all().count(), 2) - def test_invalid_create_global_unique(self): + def test_invalid_create_partij_required(self): + data = { + "partijIdentificator": { + "codeObjecttype": "natuurlijk_persoon", + "codeSoortObjectId": "bsn", + "objectId": "296648875", + "codeRegister": "brp", + }, + } + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + error = get_validation_errors(response, "identificeerdePartij") + self.assertEqual(error["code"], "required") + self.assertEqual(error["reason"], "Dit veld is vereist.") + + def test_invalid_create_duplicate_code_soort_object_id_for_partij(self): PartijIdentificatorFactory.create( partij=self.partij, - partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", - partij_identificator_object_id="296648875", - partij_identificator_code_register="brp", ) data = { @@ -2234,86 +2243,71 @@ def test_invalid_create_global_unique(self): "codeRegister": "brp", }, } + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + error = get_validation_errors(response, "__all__") + self.assertEqual(error["code"], "unique_together") + self.assertEqual( + error["reason"], + "Partij identificator met deze Partij en Soort object ID bestaat al.", + ) + + def test_invalid_create_sub_identificator_van_has_different_partij(self): sub_identificator_van = PartijIdentificatorFactory.create( - partij=self.partij, + partij=PartijFactory.create(), partij_identificator_code_objecttype="niet_natuurlijk_persoon", partij_identificator_code_soort_object_id="kvk_nummer", partij_identificator_object_id="12345678", partij_identificator_code_register="hr", ) - with self.subTest("ok_subtest_global_1"): - # sub_identificator_van is set - data["sub_identificator_van"] = {"uuid": str(sub_identificator_van.uuid)} - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - with self.subTest("failed_subtest_global_1"): - # combination sub_identificator_van and partij_identificator arleady exists - data["sub_identificator_van"] = {"uuid": str(sub_identificator_van.uuid)} - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") - self.assertEqual( - error["reason"], - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", - ) - with self.subTest("failed_subtest_global_2"): - # same partij, same data_values - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") - self.assertEqual( - error["reason"], - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", - ) - with self.subTest("failed_subtest_global_3"): - # partij = None, same data_values - data["identificeerdePartij"] = None - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") - self.assertEqual( - error["reason"], - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", - ) + data = { + "identificeerdePartij": {"uuid": str(self.partij.uuid)}, + "sub_identificator_van": {"uuid": str(sub_identificator_van.uuid)}, + "partijIdentificator": { + "codeObjecttype": "natuurlijk_persoon", + "codeSoortObjectId": "bsn", + "objectId": "296648875", + "codeRegister": "brp", + }, + } - def test_valid_update_simple_field(self): + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + error = get_validation_errors(response, "subIdentificatorVan") + self.assertEqual(error["code"], "invalid") + self.assertEqual( + error["reason"], + "Je moet een `sub_identifier_van` selecteren die tot dezelfde Partij behoort.", + ) + + def test_valid_update_partij(self): partij_identificator = PartijIdentificatorFactory.create( - partij=self.partij, + partij=PartijFactory.create(), andere_partij_identificator="anderePartijIdentificator", partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", partij_identificator_object_id="123456782", partij_identificator_code_register="brp", ) - data = { - "anderePartijIdentificator": "changed", + "identificeerdePartij": {"uuid": str(self.partij.uuid)}, } detail_url = reverse( "klantinteracties:partijidentificator-detail", kwargs={"uuid": str(partij_identificator.uuid)}, ) + # check if this partij_identificator can be assigned to self.partij + self.assertEqual(self.partij.partijidentificator_set.count(), 0) response = self.client.patch(detail_url, data) self.assertEqual(response.status_code, status.HTTP_200_OK) data = response.json() self.assertEqual(data["identificeerdePartij"]["uuid"], str(self.partij.uuid)) - self.assertEqual(data["anderePartijIdentificator"], "changed") - self.assertEqual( - data["partijIdentificator"], - { - "codeObjecttype": "natuurlijk_persoon", - "codeSoortObjectId": "bsn", - "objectId": "123456782", - "codeRegister": "brp", - }, - ) - def test_valid_update_partij(self): - partij_identificator = PartijIdentificatorFactory.create( + def test_invalid_update_partij(self): + new_partij = PartijFactory.create() + PartijIdentificatorFactory.create( + partij=new_partij, andere_partij_identificator="anderePartijIdentificator", partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", @@ -2321,20 +2315,49 @@ def test_valid_update_partij(self): partij_identificator_code_register="brp", ) + partij_identificator = PartijIdentificatorFactory.create( + partij=self.partij, + andere_partij_identificator="anderePartijIdentificator", + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="296648875", + partij_identificator_code_register="brp", + ) data = { - "identificeerdePartij": {"uuid": str(self.partij.uuid)}, + "identificeerdePartij": {"uuid": str(new_partij.uuid)}, } detail_url = reverse( "klantinteracties:partijidentificator-detail", kwargs={"uuid": str(partij_identificator.uuid)}, ) + # check if this partij_identificator can be assigned to self.partij + self.assertEqual(new_partij.partijidentificator_set.count(), 1) + self.assertEqual( + new_partij.partijidentificator_set.filter( + partij_identificator_code_soort_object_id="bsn" + ).count(), + 1, + ) + response = self.client.patch(detail_url, data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - data = response.json() - self.assertEqual(data["identificeerdePartij"]["uuid"], str(self.partij.uuid)) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + error = get_validation_errors(response, "__all__") + self.assertEqual(error["code"], "unique_together") + self.assertEqual( + error["reason"], + "Partij identificator met deze Partij en Soort object ID bestaat al.", + ) + self.assertEqual(new_partij.partijidentificator_set.count(), 1) + self.assertEqual( + new_partij.partijidentificator_set.filter( + partij_identificator_code_soort_object_id="bsn" + ).count(), + 1, + ) - def test_valid_update_unique(self): + def test_valid_update_check_uniqueness_values(self): partij_identificator = PartijIdentificatorFactory.create( partij=self.partij, andere_partij_identificator="anderePartijIdentificator", @@ -2374,7 +2397,7 @@ def test_valid_update_unique(self): }, ) - def test_invalid_update_unique_already_exists(self): + def test_invalid_update_check_uniqueness_exists(self): partij_identificator_a = PartijIdentificatorFactory.create( partij=self.partij, partij_identificator_code_objecttype="natuurlijk_persoon", @@ -2384,7 +2407,7 @@ def test_invalid_update_unique_already_exists(self): ) partij_identificator_b = PartijIdentificatorFactory.create( - partij=self.partij, + partij=PartijFactory.create(), partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", partij_identificator_object_id="296648875", @@ -2409,41 +2432,82 @@ def test_invalid_update_unique_already_exists(self): # partij_identificator already exists response = self.client.put(detail_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") + error = get_validation_errors(response, "__all__") + self.assertEqual(error["code"], "invalid") self.assertEqual( error["reason"], "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", ) - def test_invalid_update_unique_set_sub_identificator_van_self(self): - partij_identificator_a = PartijIdentificatorFactory.create( + def test_valid_check_uniqueness_sub_identificator_van(self): + PartijIdentificatorFactory.create( partij=self.partij, partij_identificator_code_objecttype="natuurlijk_persoon", partij_identificator_code_soort_object_id="bsn", - partij_identificator_object_id="123456782", + partij_identificator_object_id="296648875", partij_identificator_code_register="brp", ) - # update partij_identificator_a with partij_identificator_b data - detail_url = reverse( - "klantinteracties:partijidentificator-detail", - kwargs={"uuid": str(partij_identificator_a.uuid)}, + # Same values, but sub_identifier_van is set + partij = PartijFactory.create() + sub_identificator_van = PartijIdentificatorFactory.create( + partij=partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="12345678", + partij_identificator_code_register="hr", ) + self.assertEqual(PartijIdentificator.objects.all().count(), 2) + data = { + "identificeerdePartij": {"uuid": str(partij.uuid)}, + "sub_identificator_van": {"uuid": str(sub_identificator_van.uuid)}, + "partijIdentificator": { + "codeObjecttype": "natuurlijk_persoon", + "codeSoortObjectId": "bsn", + "objectId": "296648875", + "codeRegister": "brp", + }, + } + response = self.client.post(self.list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(PartijIdentificator.objects.all().count(), 3) + def test_invalid_check_uniqueness_sub_identificator_van(self): + sub_identificator_van = PartijIdentificatorFactory.create( + partij=self.partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="12345678", + partij_identificator_code_register="hr", + ) + PartijIdentificatorFactory.create( + partij=self.partij, + sub_identificator_van=sub_identificator_van, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="296648875", + partij_identificator_code_register="brp", + ) + self.assertEqual(PartijIdentificator.objects.all().count(), 2) + # Same values and same sub_identificator_van data = { "identificeerdePartij": {"uuid": str(self.partij.uuid)}, - "sub_identificator_van": {"uuid": str(partij_identificator_a.uuid)}, + "sub_identificator_van": {"uuid": str(sub_identificator_van.uuid)}, + "partijIdentificator": { + "codeObjecttype": "natuurlijk_persoon", + "codeSoortObjectId": "bsn", + "objectId": "296648875", + "codeRegister": "brp", + }, } - - # set sub_identificator_van self relation - response = self.client.patch(detail_url, data) + response = self.client.post(self.list_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "subIdentificatorVan") - self.assertEqual(error["code"], "invalid") + error = get_validation_errors(response, "__all__") + self.assertEqual(error["code"], "unique_together") self.assertEqual( error["reason"], - "Kan zichzelf niet selecteren als `subIdentificatorVan`.", + "Partij identificator met deze Partij en Soort object ID bestaat al.", ) + self.assertEqual(PartijIdentificator.objects.all().count(), 2) def test_vestigingsnummer_valid_create(self): sub_identificator_van = PartijIdentificatorFactory.create( @@ -2504,7 +2568,6 @@ def test_vestigingsnummer_invalid_create_without_partij(self): ) data = { - "identificeerdePartij": None, "sub_identificator_van": {"uuid": str(sub_identificator_van.uuid)}, "partijIdentificator": { "codeObjecttype": "vestiging", @@ -2516,15 +2579,9 @@ def test_vestigingsnummer_invalid_create_without_partij(self): response = self.client.post(self.list_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "subIdentificatorVan") - self.assertEqual(error["code"], "invalid") - self.assertEqual( - error["reason"], - ( - "Het is niet mogelijk om een partij_identificator te maken zonder de partij" - "waartoe deze behoort te specificeren." - ), - ) + error = get_validation_errors(response, "identificeerdePartij") + self.assertEqual(error["code"], "required") + self.assertEqual(error["reason"], "Dit veld is vereist.") def test_vestigingsnummer_invalid_create_external_partij(self): partij = PartijFactory.create() @@ -2550,11 +2607,12 @@ def test_vestigingsnummer_invalid_create_external_partij(self): response = self.client.post(self.list_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + error = get_validation_errors(response, "subIdentificatorVan") self.assertEqual(error["code"], "invalid") self.assertEqual( error["reason"], - "Je moet een `sub_identifier_van` selecteren die tot dezelfde partij behoort.", + "Je moet een `sub_identifier_van` selecteren die tot dezelfde Partij behoort.", ) # valida case sub_identificator_van partij equal vestigingsnummer partij @@ -2592,42 +2650,6 @@ def test_vestigingsnummer_invalid_create_invalid_sub_identificator_van(self): "Het is alleen mogelijk om een subIdentifierVan te selecteren met codeSoortObjectId = `kvk_nummer`.", ) - def test_vestigingsnummer_invalid_create(self): - sub_identificator_van = PartijIdentificatorFactory.create( - partij=self.partij, - partij_identificator_code_objecttype="niet_natuurlijk_persoon", - partij_identificator_code_soort_object_id="kvk_nummer", - partij_identificator_object_id="12345678", - partij_identificator_code_register="hr", - ) - - data = { - "identificeerdePartij": {"uuid": str(self.partij.uuid)}, - "sub_identificator_van": {"uuid": str(sub_identificator_van.uuid)}, - "partijIdentificator": { - "codeObjecttype": "vestiging", - "codeSoortObjectId": "vestigingsnummer", - "objectId": "296648875154", - "codeRegister": "hr", - }, - } - - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - partij_identificatoren = PartijIdentificator.objects.all() - self.assertEqual(partij_identificatoren.count(), 2) - - # create new same data - response = self.client.post(self.list_url, data) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") - self.assertEqual( - error["reason"], - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", - ) - self.assertEqual(partij_identificatoren.count(), 2) - def test_vestigingsnummer_valid_update(self): sub_identificator_van = PartijIdentificatorFactory.create( partij=self.partij, @@ -2641,7 +2663,7 @@ def test_vestigingsnummer_valid_update(self): sub_identificator_van=sub_identificator_van, partij_identificator_code_objecttype="vestiging", partij_identificator_code_soort_object_id="vestigingsnummer", - partij_identificator_object_id="12345678", + partij_identificator_object_id="111122223333", partij_identificator_code_register="hr", ) @@ -2665,7 +2687,7 @@ def test_vestigingsnummer_valid_update(self): { "codeObjecttype": "vestiging", "codeSoortObjectId": "vestigingsnummer", - "objectId": "12345678", + "objectId": "111122223333", "codeRegister": "hr", }, ) @@ -2683,7 +2705,7 @@ def test_vestigingsnummer_invalid_update_set_sub_identificator_van_null(self): sub_identificator_van=sub_identificator_van, partij_identificator_code_objecttype="vestiging", partij_identificator_code_soort_object_id="vestigingsnummer", - partij_identificator_object_id="12345678", + partij_identificator_object_id="111122223333", partij_identificator_code_register="hr", ) @@ -2739,64 +2761,70 @@ def test_invalid_vestigingsnummer_and_kvk_nummer_combination_unique(self): response = self.client.post(self.list_url, data) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") + self.assertEqual(len(response.data["invalid_params"]), 2) + self.assertEqual(response.data["invalid_params"][0]["code"], "unique_together") self.assertEqual( - error["reason"], + response.data["invalid_params"][0]["reason"], + "Partij identificator met deze Partij en Soort object ID bestaat al.", + ) + self.assertEqual(response.data["invalid_params"][1]["code"], "invalid") + self.assertEqual( + response.data["invalid_params"][1]["reason"], "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", ) - def test_vestigingsnummer_invalid_update_unique(self): - sub_identificator_van_a = PartijIdentificatorFactory.create( + def test_valid_protect_delete(self): + partij_identificator = PartijIdentificatorFactory.create( partij=self.partij, partij_identificator_code_objecttype="niet_natuurlijk_persoon", partij_identificator_code_soort_object_id="kvk_nummer", partij_identificator_object_id="12345678", partij_identificator_code_register="hr", ) - sub_identificator_van_b = PartijIdentificatorFactory.create( + detail_url = reverse( + "klantinteracties:partijidentificator-detail", + kwargs={"uuid": str(partij_identificator.uuid)}, + ) + self.assertEqual(PartijIdentificator.objects.all().count(), 1) + response = self.client.delete(detail_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(PartijIdentificator.objects.all().count(), 0) + + def test_invalid_protect_delete(self): + sub_identificator_van = PartijIdentificatorFactory.create( partij=self.partij, partij_identificator_code_objecttype="niet_natuurlijk_persoon", partij_identificator_code_soort_object_id="kvk_nummer", partij_identificator_object_id="12345678", partij_identificator_code_register="hr", ) - partij_identificator_a = PartijIdentificatorFactory.create( - partij=self.partij, - sub_identificator_van=sub_identificator_van_a, - partij_identificator_code_objecttype="vestiging", - partij_identificator_code_soort_object_id="vestigingsnummer", - partij_identificator_object_id="12345678", - partij_identificator_code_register="hr", - ) PartijIdentificatorFactory.create( partij=self.partij, - sub_identificator_van=sub_identificator_van_b, + sub_identificator_van=sub_identificator_van, partij_identificator_code_objecttype="vestiging", partij_identificator_code_soort_object_id="vestigingsnummer", - partij_identificator_object_id="12345678", + partij_identificator_object_id="296648875154", partij_identificator_code_register="hr", ) - - self.assertEqual(PartijIdentificator.objects.count(), 4) - - data = { - "sub_identificator_van": {"uuid": str(sub_identificator_van_b.uuid)}, - } detail_url = reverse( "klantinteracties:partijidentificator-detail", - kwargs={"uuid": str(partij_identificator_a.uuid)}, + kwargs={"uuid": str(sub_identificator_van.uuid)}, ) - - response = self.client.patch(detail_url, data) + self.assertEqual(PartijIdentificator.objects.all().count(), 2) + response = self.client.delete(detail_url) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) error = get_validation_errors(response, "nonFieldErrors") - self.assertEqual(error["code"], "unique") + self.assertEqual(error["code"], "invalid") self.assertEqual( error["reason"], - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", + ( + "Cannot delete some instances of model 'PartijIdentificator' because they are" + " referenced through protected foreign keys: 'PartijIdentificator.sub_identificator_van'." + ), ) + self.assertEqual(PartijIdentificator.objects.all().count(), 2) + class CategorieRelatieTests(APITestCase): def test_list_categorie_relatie(self): diff --git a/src/openklant/components/klantinteracties/api/viewsets/partijen.py b/src/openklant/components/klantinteracties/api/viewsets/partijen.py index 46624d77..6f8d0e78 100644 --- a/src/openklant/components/klantinteracties/api/viewsets/partijen.py +++ b/src/openklant/components/klantinteracties/api/viewsets/partijen.py @@ -26,6 +26,7 @@ from openklant.components.token.authentication import TokenAuthentication from openklant.components.token.permission import TokenPermissions from openklant.components.utils.mixins import ExpandMixin +from openklant.utils.decorators import handle_db_exceptions @extend_schema(tags=["partijen"]) @@ -252,3 +253,7 @@ class PartijIdentificatorViewSet(viewsets.ModelViewSet): ] authentication_classes = (TokenAuthentication,) permission_classes = (TokenPermissions,) + + @handle_db_exceptions + def destroy(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) diff --git a/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van.py b/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van.py deleted file mode 100644 index 9f16f6ad..00000000 --- a/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.2.17 on 2025-02-20 09:51 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "klantinteracties", - "0027_alter_betrokkene_bezoekadres_nummeraanduiding_id_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="partijidentificator", - name="sub_identificator_van", - field=models.ForeignKey( - blank=True, - help_text=( - "The parent PartijIdentificator under which this PartijIdentificator is unique " - "(e.g. the parent identificator could specify a KVK number and the child identificator " - "could specify a vestigingsnummer that is unique for the KVK number)." - ), - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="parent_partij_identificator", - to="klantinteracties.partijidentificator", - verbose_name="sub identificator van", - ), - ), - ] diff --git a/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van_and_more.py b/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van_and_more.py new file mode 100644 index 00000000..fba66f98 --- /dev/null +++ b/src/openklant/components/klantinteracties/migrations/0028_partijidentificator_sub_identificator_van_and_more.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.17 on 2025-02-26 13:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "klantinteracties", + "0027_alter_betrokkene_bezoekadres_nummeraanduiding_id_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="partijidentificator", + name="sub_identificator_van", + field=models.ForeignKey( + blank=True, + help_text="The parent PartijIdentificator under which this PartijIdentificator is unique (e.g. the parent identificator could specify a KVK number and the child identificator could specify a vestigingsnummer that is unique for the KVK number).", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="parent_partij_identificator", + to="klantinteracties.partijidentificator", + verbose_name="sub identificator van", + ), + ), + migrations.AddConstraint( + model_name="partijidentificator", + constraint=models.UniqueConstraint( + condition=models.Q(("sub_identificator_van__isnull", True)), + fields=( + "partij_identificator_code_objecttype", + "partij_identificator_code_soort_object_id", + "partij_identificator_object_id", + "partij_identificator_code_register", + ), + name="non_scoped_identificator_globally_unique", + violation_error_message="`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", + ), + ), + migrations.AddConstraint( + model_name="partijidentificator", + constraint=models.UniqueConstraint( + fields=("partij", "partij_identificator_code_soort_object_id"), + name="non_scoped_identificator_locally_unique", + violation_error_message="`CodeSoortObjectId` moet uniek zijn voor de Partij.", + ), + ), + migrations.AddConstraint( + model_name="partijidentificator", + constraint=models.UniqueConstraint( + condition=models.Q(("sub_identificator_van__isnull", False)), + fields=( + "sub_identificator_van", + "partij_identificator_code_objecttype", + "partij_identificator_code_soort_object_id", + "partij_identificator_object_id", + "partij_identificator_code_register", + ), + name="scoped_identificator_globally_unique", + violation_error_message="`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", + ), + ), + ] diff --git a/src/openklant/components/klantinteracties/models/partijen.py b/src/openklant/components/klantinteracties/models/partijen.py index b7cdb5ac..bb63f5e9 100644 --- a/src/openklant/components/klantinteracties/models/partijen.py +++ b/src/openklant/components/klantinteracties/models/partijen.py @@ -336,7 +336,7 @@ class PartijIdentificator(models.Model): ) sub_identificator_van = models.ForeignKey( "self", - on_delete=models.SET_NULL, + on_delete=models.PROTECT, verbose_name=_("sub identificator van"), help_text=_( "The parent PartijIdentificator under which this PartijIdentificator is unique " @@ -419,8 +419,73 @@ class Meta: verbose_name = _("partij identificator") verbose_name_plural = _("partij identificatoren") - def __str__(self): - soort_object = self.partij_identificator_code_soort_object_id - object = self.partij_identificator_object_id + constraints = [ + models.UniqueConstraint( + fields=[ + "partij_identificator_code_objecttype", + "partij_identificator_code_soort_object_id", + "partij_identificator_object_id", + "partij_identificator_code_register", + ], + condition=models.Q(sub_identificator_van__isnull=True), + name="non_scoped_identificator_globally_unique", + violation_error_message=_( + "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie." + ), + ), + models.UniqueConstraint( + fields=[ + "partij", + "partij_identificator_code_soort_object_id", + ], + name="non_scoped_identificator_locally_unique", + violation_error_message=_( + "`CodeSoortObjectId` moet uniek zijn voor de Partij." + ), + ), + models.UniqueConstraint( + fields=[ + "sub_identificator_van", + "partij_identificator_code_objecttype", + "partij_identificator_code_soort_object_id", + "partij_identificator_object_id", + "partij_identificator_code_register", + ], + condition=models.Q(sub_identificator_van__isnull=False), + name="scoped_identificator_globally_unique", + violation_error_message=_( + "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie." + ), + ), + ] + + def clean_sub_identificator_van(self): + if self.sub_identificator_van: + if self.sub_identificator_van == self: + raise ValidationError( + { + "sub_identificator_van": _( + "Kan zichzelf niet selecteren als `subIdentificatorVan`." + ) + } + ) + + if self.sub_identificator_van.partij != self.partij: + raise ValidationError( + { + "sub_identificator_van": _( + "Je moet een `sub_identifier_van` selecteren die tot dezelfde Partij behoort." + ) + } + ) - return f"{soort_object} - {object}" + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) + + def clean(self): + super().clean() + self.clean_sub_identificator_van() + + def __str__(self): + return f"{self.partij_identificator_code_soort_object_id} - {self.partij_identificator_object_id}" diff --git a/src/openklant/components/klantinteracties/models/tests/test_partijen.py b/src/openklant/components/klantinteracties/models/tests/test_partijen.py new file mode 100644 index 00000000..94cb1da7 --- /dev/null +++ b/src/openklant/components/klantinteracties/models/tests/test_partijen.py @@ -0,0 +1,238 @@ +from django.core.exceptions import ValidationError +from django.db.models import ProtectedError +from django.test import TestCase + +from openklant.components.klantinteracties.models.partijen import PartijIdentificator +from openklant.components.klantinteracties.models.tests.factories.partijen import ( + PartijFactory, +) + + +class PartijIdentificatorModelConstraints(TestCase): + + def test_valid_globally_unique(self): + partij_a = PartijFactory.create() + partij_identificator_a = PartijIdentificator.objects.create( + partij=partij_a, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="12345678", + partij_identificator_code_register="hr", + ) + + partij_b = PartijFactory.create() + partij_identificator_b = PartijIdentificator.objects.create( + partij=partij_b, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + + # different sub_identificator_van same values + PartijIdentificator.objects.create( + partij=partij_a, + sub_identificator_van=partij_identificator_a, + partij_identificator_code_objecttype="vestiging", + partij_identificator_code_soort_object_id="vestigingsnummer", + partij_identificator_object_id="123412341234", + partij_identificator_code_register="hr", + ) + + PartijIdentificator.objects.create( + partij=partij_b, + sub_identificator_van=partij_identificator_b, + partij_identificator_code_objecttype="vestiging", + partij_identificator_code_soort_object_id="vestigingsnummer", + partij_identificator_object_id="123412341234", + partij_identificator_code_register="hr", + ) + + # different values same sub_identificator_van + PartijIdentificator.objects.create( + partij=partij_a, + sub_identificator_van=partij_identificator_a, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + PartijIdentificator.objects.create( + partij=partij_b, + sub_identificator_van=partij_identificator_b, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + + def test_non_scoped_identificator_globally_unique(self): + partij = PartijFactory.create() + PartijIdentificator.objects.create( + partij=partij, + sub_identificator_van=None, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + with self.assertRaises(ValidationError) as error: + PartijIdentificator.objects.create( + partij=partij, + sub_identificator_van=None, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + self.assertEqual( + error.exception.message_dict, + { + "__all__": [ + "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", + "Partij identificator met deze Partij en Soort object ID bestaat al.", + ] + }, + ) + + def test_scoped_identificator_globally_unique(self): + partij = PartijFactory.create() + sub_identificator_van = PartijIdentificator.objects.create( + partij=partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + PartijIdentificator.objects.create( + partij=partij, + sub_identificator_van=sub_identificator_van, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + + with self.assertRaises(ValidationError) as error: + PartijIdentificator.objects.create( + partij=partij, + sub_identificator_van=sub_identificator_van, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + self.assertEqual( + error.exception.message_dict, + { + "__all__": [ + "Partij identificator met deze Partij en Soort object ID bestaat al.", + "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie.", + ] + }, + ) + + def test_non_scoped_identificator_locally_unique(self): + partij = PartijFactory.create() + PartijIdentificator.objects.create( + partij=partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + + with self.assertRaises(ValidationError) as error: + PartijIdentificator.objects.create( + partij=partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="11112222", + partij_identificator_code_register="hr", + ) + + self.assertEqual( + error.exception.message_dict, + { + "__all__": [ + "Partij identificator met deze Partij en Soort object ID bestaat al." + ] + }, + ) + + def test_protected_delete(self): + partij = PartijFactory.create() + sub_identificator_van = PartijIdentificator.objects.create( + partij=partij, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + PartijIdentificator.objects.create( + partij=partij, + sub_identificator_van=sub_identificator_van, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + with self.assertRaises(ProtectedError) as error: + PartijIdentificator.objects.filter(uuid=sub_identificator_van.uuid).delete() + + self.assertTrue( + "Cannot delete some instances of model 'PartijIdentificator'" + in error.exception.args[0], + ) + + +class PartijIdentificatorModelClean(TestCase): + + def test_clean_sub_identificator_van_different_partij(self): + partij_a = PartijFactory.create() + partij_b = PartijFactory.create() + sub_identificator_van = PartijIdentificator.objects.create( + partij=partij_a, + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + with self.assertRaises(ValidationError) as error: + PartijIdentificator.objects.create( + partij=partij_b, + sub_identificator_van=sub_identificator_van, + partij_identificator_code_objecttype="natuurlijk_persoon", + partij_identificator_code_soort_object_id="bsn", + partij_identificator_object_id="123456782", + partij_identificator_code_register="brp", + ) + + self.assertEqual( + error.exception.message_dict, + { + "sub_identificator_van": [ + "Je moet een `sub_identifier_van` selecteren die tot dezelfde Partij behoort." + ] + }, + ) + + def test_clean_sub_identificator_van_different_self_related(self): + partij_identificator = PartijIdentificator.objects.create( + partij=PartijFactory.create(), + partij_identificator_code_objecttype="niet_natuurlijk_persoon", + partij_identificator_code_soort_object_id="kvk_nummer", + partij_identificator_object_id="87654321", + partij_identificator_code_register="hr", + ) + with self.assertRaises(ValidationError) as error: + partij_identificator.sub_identificator_van = partij_identificator + partij_identificator.save() + self.assertEqual( + error.exception.message_dict, + { + "sub_identificator_van": [ + "Kan zichzelf niet selecteren als `subIdentificatorVan`." + ] + }, + ) diff --git a/src/openklant/components/klantinteracties/models/validators.py b/src/openklant/components/klantinteracties/models/validators.py index ae92ba74..0a3c161d 100644 --- a/src/openklant/components/klantinteracties/models/validators.py +++ b/src/openklant/components/klantinteracties/models/validators.py @@ -7,10 +7,7 @@ validate_rsin, ) -from openklant.components.klantinteracties.models.partijen import ( - Partij, - PartijIdentificator, -) +from openklant.components.klantinteracties.models.partijen import PartijIdentificator from .constants import ( PartijIdentificatorCodeObjectType, @@ -25,55 +22,24 @@ class PartijIdentificatorUniquenessValidator: def __init__( self, - partij_identificator: dict | None = {}, + code_soort_object_id: str | None = "", sub_identificator_van: PartijIdentificator | None = None, - instance: PartijIdentificator | None = None, - partij: Partij | None = None, ): - self.partij_identificator = partij_identificator + self.code_soort_object_id = code_soort_object_id self.sub_identificator_van = sub_identificator_van - self.instance = instance - self.partij = partij - self.queryset = PartijIdentificator.objects.all() - - if self.instance: - self.queryset = self.queryset.exclude(pk=self.instance.pk) - - if not self.partij_identificator: - raise ValueError("partij_identificator is required") def __call__(self): - self.validate_not_self_assigned() if ( - self.partij_identificator["code_soort_object_id"] + self.code_soort_object_id == PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value ): self.validate_sub_identificator_van_for_vestigingsnummer() - self.validate_unique_partij_identificator_locally() - self.validate_unique_partij_identificator_globally() - - def validate_not_self_assigned(self): - """ - Validation that the current instance does not assign itself as 'sub_identificator_van'. - """ - if self.sub_identificator_van and self.sub_identificator_van == self.instance: - raise ValidationError( - { - "sub_identificator_van": _( - "Kan zichzelf niet selecteren als `subIdentificatorVan`." - ) - } - ) def validate_sub_identificator_van_for_vestigingsnummer(self): """ - Validation that when the partij_identificator has CodeSoortObjectId = `vestigingsnummer`: - - if the `sub_identificator_van` is not selected - - the `sub_identificator_van` must have CodeSoortObjectId = `kvk_nummer`. - - cannot be assigned to a null Partij - - cannot be assigned to a Partij that doesn't have another partij_identificator - with CodeSoortObjectId = `kvk_nummer` - + - `sub_identificator_van` is required + - `sub_identificator_van` must have CodeSoortObjectId = `kvk_nummer` """ if not self.sub_identificator_van: raise ValidationError( @@ -98,88 +64,6 @@ def validate_sub_identificator_van_for_vestigingsnummer(self): } ) - if not self.partij: - raise ValidationError( - { - "sub_identificator_van": _( - "Het is niet mogelijk om een partij_identificator te maken zonder de partij" - "waartoe deze behoort te specificeren." - ) - } - ) - - if ( - not self.queryset.filter(partij=self.partij) - .filter( - partij_identificator_code_soort_object_id=PartijIdentificatorCodeSoortObjectId.kvk_nummer.value - ) - .exists() - ): - raise ValidationError( - { - "sub_identificator_van": _( - "Je moet een `sub_identifier_van` selecteren die tot dezelfde partij behoort." - ) - } - ) - - def validate_unique_partij_identificator_locally(self): - """ - Validation that a single Partij can only have a single partij_identificator_code_soort_object_id type locally - """ - return # TODO unlock after feedback - if ( - self.partij - and self.queryset.filter(partij=self.partij) - .filter( - partij_identificator_code_soort_object_id=self.partij_identificator[ - "code_soort_object_id" - ] - ) - .exists() - ): - raise ValidationError( - { - "partij_identificator_code_soort_object_id": _( - "Er is al een PartijIdentificator met codeSoortObjectId = " - "`{code_soort_object_id}` voor deze Partij." - ).format( - code_soort_object_id=self.partij_identificator[ - "code_soort_object_id" - ] - ) - } - ) - - def validate_unique_partij_identificator_globally(self): - """ - Validation that a single partij_identifier combination occurs only once globally - """ - filters = { - "partij_identificator_code_objecttype": self.partij_identificator[ - "code_objecttype" - ], - "partij_identificator_code_soort_object_id": self.partij_identificator[ - "code_soort_object_id" - ], - "partij_identificator_object_id": self.partij_identificator["object_id"], - "partij_identificator_code_register": self.partij_identificator[ - "code_register" - ], - } - - if self.sub_identificator_van is None: - filters["sub_identificator_van__isnull"] = True - else: - filters["sub_identificator_van"] = self.sub_identificator_van - if self.queryset.filter(**filters).exists(): - raise ValidationError( - _( - "`PartijIdentificator` moet uniek zijn, er bestaat er al een met deze gegevenscombinatie." - ), - code="unique", - ) - class PartijIdentificatorTypesValidator: """ @@ -227,15 +111,13 @@ class PartijIdentificatorTypesValidator: ], } - def __init__(self, partij_identificator: dict) -> None: + def __call__(self, partij_identificator: dict) -> None: """Initialize validator""" self.code_register = partij_identificator["code_register"] self.code_objecttype = partij_identificator["code_objecttype"] self.code_soort_object_id = partij_identificator["code_soort_object_id"] self.object_id = partij_identificator["object_id"] - def __call__(self) -> None: - """Run all validations""" self.validate_code_objecttype() self.validate_code_soort_object_id() self.validate_object_id() diff --git a/src/openklant/components/klantinteracties/openapi.yaml b/src/openklant/components/klantinteracties/openapi.yaml index fb0120e5..c36eaa53 100644 --- a/src/openklant/components/klantinteracties/openapi.yaml +++ b/src/openklant/components/klantinteracties/openapi.yaml @@ -4754,6 +4754,11 @@ components: nullable: true description: Gegevens die een partij in een basisregistratie of ander extern register uniek identificeren. + subIdentificatorVan: + allOf: + - $ref: '#/components/schemas/PartijIdentificatorForeignkey' + nullable: true + description: Relatie sub_identificator_van required: - identificeerdePartij - partijIdentificator @@ -4804,6 +4809,11 @@ components: oneOf: - $ref: '#/components/schemas/CodeRegisterEnum' - $ref: '#/components/schemas/BlankEnum' + required: + - codeObjecttype + - codeRegister + - codeSoortObjectId + - objectId PartijPolymorphic: type: object properties: @@ -5296,6 +5306,11 @@ components: nullable: true description: Gegevens die een partij in een basisregistratie of ander extern register uniek identificeren. + subIdentificatorVan: + allOf: + - $ref: '#/components/schemas/PartijIdentificatorForeignkey' + nullable: true + description: Relatie sub_identificator_van PatchedRekeningnummer: type: object properties: diff --git a/src/openklant/components/klantinteracties/tests/test_validators.py b/src/openklant/components/klantinteracties/tests/test_validators.py index 39dbe16d..bf7683e2 100644 --- a/src/openklant/components/klantinteracties/tests/test_validators.py +++ b/src/openklant/components/klantinteracties/tests/test_validators.py @@ -6,31 +6,26 @@ PartijIdentificatorCodeRegister, PartijIdentificatorCodeSoortObjectId, ) -from openklant.components.klantinteracties.models.tests.factories.partijen import ( - PartijFactory, - PartijIdentificatorFactory, -) from openklant.components.klantinteracties.models.validators import ( PartijIdentificatorTypesValidator, - PartijIdentificatorUniquenessValidator, ) class PartijIdentificatorTypesValidatorTests(TestCase): def test_valid(self): - PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, "object_id": "296648875", "code_register": PartijIdentificatorCodeRegister.brp.value, } - )() + ) # Start section validate_code_objecttype def test_valid_code_objecttype_null(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": "", "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -38,10 +33,9 @@ def test_valid_code_objecttype_null(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_code_objecttype() def test_valid_code_objecttype_top_level_null_or_overig(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -49,9 +43,8 @@ def test_valid_code_objecttype_top_level_null_or_overig(self): "code_register": "", } ) - validator.validate_code_objecttype() - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -59,11 +52,10 @@ def test_valid_code_objecttype_top_level_null_or_overig(self): "code_register": PartijIdentificatorCodeRegister.overig.value, } ) - validator.validate_code_objecttype() def test_invalid_code_objecttype_not_found_in_top_level(self): with self.assertRaises(ValidationError) as error: - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.niet_natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -71,7 +63,6 @@ def test_invalid_code_objecttype_not_found_in_top_level(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_code_objecttype() details = error.exception.message_dict self.assertEqual( @@ -82,7 +73,7 @@ def test_invalid_code_objecttype_not_found_in_top_level(self): # Start section validate_code_soort_object_id def test_valid_code_soort_object_id_null(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": "", @@ -90,11 +81,10 @@ def test_valid_code_soort_object_id_null(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_code_soort_object_id() def test_invalid_code_soort_object_id_not_found_in_top_level(self): with self.assertRaises(ValidationError) as error: - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.rsin.value, @@ -102,7 +92,6 @@ def test_invalid_code_soort_object_id_not_found_in_top_level(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_code_soort_object_id() details = error.exception.message_dict self.assertEqual( @@ -113,7 +102,7 @@ def test_invalid_code_soort_object_id_not_found_in_top_level(self): # Start section validate_object_id def test_valid_object_id_null(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -121,10 +110,9 @@ def test_valid_object_id_null(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() def test_valid_object_id_top_level_null_or_overig(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": "", @@ -132,9 +120,8 @@ def test_valid_object_id_top_level_null_or_overig(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.overig.value, @@ -142,10 +129,9 @@ def test_valid_object_id_top_level_null_or_overig(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() def test_valid_object_id_bsn(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -153,10 +139,9 @@ def test_valid_object_id_bsn(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() def test_valid_object_id_vestigingsnummer(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.vestiging.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value, @@ -164,10 +149,9 @@ def test_valid_object_id_vestigingsnummer(self): "code_register": PartijIdentificatorCodeRegister.hr.value, } ) - validator.validate_object_id() def test_valid_object_id_rsin(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.niet_natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.rsin.value, @@ -175,10 +159,9 @@ def test_valid_object_id_rsin(self): "code_register": PartijIdentificatorCodeRegister.hr.value, } ) - validator.validate_object_id() def test_valid_object_id_kvk_nummer(self): - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.niet_natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.kvk_nummer.value, @@ -186,11 +169,10 @@ def test_valid_object_id_kvk_nummer(self): "code_register": PartijIdentificatorCodeRegister.hr.value, } ) - validator.validate_object_id() def test_invalid_object_id_len_bsn(self): with self.assertRaises(ValidationError) as error: - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -198,7 +180,6 @@ def test_invalid_object_id_len_bsn(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() details = error.exception.message_dict self.assertEqual( @@ -208,7 +189,7 @@ def test_invalid_object_id_len_bsn(self): def test_invalid_object_id_digit_bsn(self): with self.assertRaises(ValidationError) as error: - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -216,7 +197,6 @@ def test_invalid_object_id_digit_bsn(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() details = error.exception.message_dict self.assertEqual( @@ -226,7 +206,7 @@ def test_invalid_object_id_digit_bsn(self): def test_invalid_object_id_proef11_bsn(self): with self.assertRaises(ValidationError) as error: - validator = PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, @@ -234,7 +214,6 @@ def test_invalid_object_id_proef11_bsn(self): "code_register": PartijIdentificatorCodeRegister.brp.value, } ) - validator.validate_object_id() details = error.exception.message_dict self.assertEqual( @@ -324,14 +303,14 @@ def test_allowed_cases(self): ], ] for case in valid_cases: - PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_register": case[0], "code_objecttype": case[1], "code_soort_object_id": case[2], "object_id": case[3], }, - )() + ) def test_not_allowed_cases(self): invalid_cases = [ @@ -356,123 +335,11 @@ def test_not_allowed_cases(self): ] for case in invalid_cases: with self.assertRaises(ValidationError): - PartijIdentificatorTypesValidator( + PartijIdentificatorTypesValidator()( partij_identificator={ "code_register": case[0], "code_objecttype": case[1], "code_soort_object_id": case[2], "object_id": case[3], } - )() - - -class PartijIdentificatorUniquenessValidatorTests(TestCase): - - def test_valid_global_uniqueness(self): - PartijIdentificatorUniquenessValidator( - partij_identificator={ - "code_objecttype": PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, - "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.bsn.value, - "object_id": "296648875", - "code_register": PartijIdentificatorCodeRegister.brp.value, - }, - sub_identificator_van=None, - instance=None, - )() - - def test_valid_relation_sub_identificator_van(self): - # check self relation and sub_identificator_van allowed cases - partij = PartijFactory.create() - sub_identificator_van = PartijIdentificatorFactory.create( - partij=partij, - partij_identificator_code_objecttype=PartijIdentificatorCodeObjectType.niet_natuurlijk_persoon.value, - partij_identificator_code_soort_object_id=PartijIdentificatorCodeSoortObjectId.kvk_nummer.value, - partij_identificator_object_id="12345678", - partij_identificator_code_register=PartijIdentificatorCodeRegister.hr.value, - ) - PartijIdentificatorUniquenessValidator( - partij_identificator={ - "code_objecttype": PartijIdentificatorCodeObjectType.vestiging.value, - "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value, - "object_id": "296648875154", - "code_register": PartijIdentificatorCodeRegister.hr.value, - }, - sub_identificator_van=sub_identificator_van, - instance=None, - partij=partij, - )() - - def test_invalid_self_relation_sub_identificator_van(self): - partij_identificator = PartijIdentificatorFactory.create( - partij_identificator_code_objecttype=PartijIdentificatorCodeObjectType.niet_natuurlijk_persoon.value, - partij_identificator_code_soort_object_id=PartijIdentificatorCodeSoortObjectId.kvk_nummer.value, - partij_identificator_object_id="12345678", - partij_identificator_code_register=PartijIdentificatorCodeRegister.hr.value, - ) - with self.assertRaises(ValidationError) as error: - PartijIdentificatorUniquenessValidator( - partij_identificator={ - "code_objecttype": PartijIdentificatorCodeObjectType.vestiging.value, - "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value, - "object_id": "296648875154", - "code_register": PartijIdentificatorCodeRegister.hr.value, - }, - sub_identificator_van=partij_identificator, - instance=partij_identificator, - )() - - details = error.exception.message_dict - self.assertEqual( - details["sub_identificator_van"][0], - "Kan zichzelf niet selecteren als `subIdentificatorVan`.", - ) - - def test_vestigingsnummer_requires_sub_identificator_van_with_kvk_nummer(self): - # sub_identificator_van type not allowed - sub_identificator_van = PartijIdentificatorFactory.create( - partij_identificator_code_objecttype=PartijIdentificatorCodeObjectType.natuurlijk_persoon.value, - partij_identificator_code_soort_object_id=PartijIdentificatorCodeSoortObjectId.bsn.value, - partij_identificator_object_id="296648875", - partij_identificator_code_register=PartijIdentificatorCodeRegister.brp.value, - ) - with self.assertRaises(ValidationError) as error: - PartijIdentificatorUniquenessValidator( - partij_identificator={ - "code_objecttype": PartijIdentificatorCodeObjectType.vestiging.value, - "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value, - "object_id": "296648875154", - "code_register": PartijIdentificatorCodeRegister.hr.value, - }, - sub_identificator_van=sub_identificator_van, - instance=None, - )() - - details = error.exception.message_dict - self.assertEqual( - details["sub_identificator_van"][0], - "Het is alleen mogelijk om een subIdentifierVan te selecteren met codeSoortObjectId = `kvk_nummer`.", - ) - - def test_invalid_partij_identificator_vestigingsnummer_require_sub_identificator_van( - self, - ): - with self.assertRaises(ValidationError) as error: - PartijIdentificatorUniquenessValidator( - partij_identificator={ - "code_objecttype": PartijIdentificatorCodeObjectType.vestiging.value, - "code_soort_object_id": PartijIdentificatorCodeSoortObjectId.vestigingsnummer.value, - "object_id": "296648875154", - "code_register": PartijIdentificatorCodeRegister.hr.value, - }, - sub_identificator_van=None, - instance=None, - )() - - details = error.exception.message_dict - self.assertEqual( - details["sub_identificator_van"][0], - ( - "Voor een PartijIdentificator met codeSoortObjectId = `vestigingsnummer` is het verplicht om" - " een `sub_identifier_van` met codeSoortObjectId = `kvk_nummer` te kiezen." - ), - ) + ) diff --git a/src/openklant/utils/decorators.py b/src/openklant/utils/decorators.py new file mode 100644 index 00000000..8b33cad2 --- /dev/null +++ b/src/openklant/utils/decorators.py @@ -0,0 +1,23 @@ +from functools import wraps + +from django.core.exceptions import ValidationError as DjangoValidationError +from django.db import models + +from rest_framework.exceptions import ValidationError +from rest_framework.fields import get_error_detail + + +def handle_db_exceptions(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except models.ProtectedError as protected_err: + raise ValidationError( + {"nonFieldErrors": protected_err.args[0]}, + code="invalid", + ) + except DjangoValidationError as validation_err: + raise ValidationError(get_error_detail(validation_err)) + + return wrapper