diff --git a/src/openklant/components/klantinteracties/admin/partijen.py b/src/openklant/components/klantinteracties/admin/partijen.py index f37f53db..5ea556d2 100644 --- a/src/openklant/components/klantinteracties/admin/partijen.py +++ b/src/openklant/components/klantinteracties/admin/partijen.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer + from ..models.constants import SoortPartij from ..models.digitaal_adres import DigitaalAdres from ..models.klantcontacten import Betrokkene @@ -40,6 +42,12 @@ class DigitaalAdresInlineAdmin(admin.StackedInline): extra = 0 +class RekeningnummerInlineAdmin(admin.StackedInline): + readonly_fields = ("uuid",) + model = Rekeningnummer + extra = 0 + + class PersoonInlineAdmin(admin.StackedInline): model = Persoon extra = 0 @@ -83,6 +91,7 @@ class PartijAdmin(admin.ModelAdmin): ContactpersoonInlineAdmin, OrganisatieInlineAdmin, DigitaalAdresInlineAdmin, + RekeningnummerInlineAdmin, BetrokkeneInlineAdmin, VertegenwoordigdenInlineAdmin, ) diff --git a/src/openklant/components/klantinteracties/api/serializers/partijen.py b/src/openklant/components/klantinteracties/api/serializers/partijen.py index 5c3b86e2..4b246708 100644 --- a/src/openklant/components/klantinteracties/api/serializers/partijen.py +++ b/src/openklant/components/klantinteracties/api/serializers/partijen.py @@ -41,6 +41,7 @@ Persoon, Vertegenwoordigden, ) +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer class PartijForeignkeyBaseSerializer(serializers.HyperlinkedModelSerializer): @@ -417,6 +418,10 @@ def create(self, validated_data): class PartijSerializer(NestedGegevensGroepMixin, PolymorphicSerializer): + from openklant.components.klantinteracties.api.serializers.rekeningnummers import ( + RekeningnummerForeignKeySerializer, + ) + discriminator = Discriminator( discriminator_field="soort_partij", mapping={ @@ -445,7 +450,7 @@ class PartijSerializer(NestedGegevensGroepMixin, PolymorphicSerializer): required=True, allow_null=True, help_text=_( - "Digitaal adres dat een partij verstrekte voor gebruik bij " + "Digitaal adresen dat een partij verstrekte voor gebruik bij " "toekomstig contact met de gemeente." ), source="digitaaladres_set", @@ -458,6 +463,18 @@ class PartijSerializer(NestedGegevensGroepMixin, PolymorphicSerializer): "Digitaal adres waarop een partij bij voorkeur door de gemeente benaderd wordt." ), ) + rekeningnummers = RekeningnummerForeignKeySerializer( + required=True, + allow_null=True, + help_text=_("Rekeningnummers van een partij"), + source="rekeningnummer_set", + many=True, + ) + voorkeurs_rekeningnummer = RekeningnummerForeignKeySerializer( + required=True, + allow_null=True, + help_text=_("Rekeningsnummer die een partij bij voorkeur gebruikt."), + ) vertegenwoordigden = serializers.SerializerMethodField( help_text=_("Partijen die een andere partijen vertegenwoordigden."), ) @@ -507,6 +524,8 @@ class Meta: "digitale_adressen", "voorkeurs_digitaal_adres", "vertegenwoordigden", + "rekeningnummers", + "voorkeurs_rekeningnummer", "partij_identificatoren", "soort_partij", "indicatie_geheimhouding", @@ -606,6 +625,82 @@ def update(self, instance, validated_data): validated_data["voorkeurs_digitaal_adres"] = voorkeurs_digitaal_adres + if "rekeningnummer_set" in validated_data: + existing_rekeningnummers = instance.rekeningnummer_set.all() + rekeningnummers_uuids = [ + rekeningnummer["uuid"] + for rekeningnummer in validated_data.pop("rekeningnummer_set") + ] + + # unset relation of rekeningnummer that weren't given with the update + for rekeningnummer in existing_rekeningnummers: + if rekeningnummer.uuid not in rekeningnummers_uuids: + rekeningnummer.partij = None + rekeningnummer.save() + + # create relation between rekeningnummer and partij of new entries + for rekeninnummers_uuid in rekeningnummers_uuids: + if rekeninnummers_uuid not in existing_rekeningnummers.values_list( + "uuid", flat=True + ): + rekeningnummer = Rekeningnummer.objects.get( + uuid=rekeninnummers_uuid + ) + rekeningnummer.partij = instance + rekeningnummer.save() + + if "voorkeurs_rekeningnummer" in validated_data: + if voorkeurs_rekeningnummer := validated_data.pop( + "voorkeurs_rekeningnummer", None + ): + voorkeurs_rekeningnummer_uuid = voorkeurs_rekeningnummer.get("uuid") + match (method): + case "PUT": + if len(rekeningnummers_uuids) == 0: + raise serializers.ValidationError( + { + "voorkeurs_rekeningnummer": _( + "voorkeursRekeningnummer mag niet meegegeven worden " + "als rekeningnummers leeg is." + ) + } + ) + if voorkeurs_rekeningnummer_uuid not in rekeningnummers_uuids: + raise serializers.ValidationError( + { + "voorkeurs_rekeningnummer": _( + "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn." + ) + } + ) + case "PATCH": + if ( + voorkeurs_rekeningnummer_uuid + not in instance.rekeningnummer_set.all().values_list( + "uuid", flat=True + ) + ): + raise serializers.ValidationError( + { + "voorkeurs_rekeningnummer": _( + "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn." + ) + } + ) + + voorkeurs_rekeningnummer = Rekeningnummer.objects.get( + uuid=str(voorkeurs_rekeningnummer_uuid) + ) + + validated_data["voorkeurs_rekeningnummer"] = voorkeurs_rekeningnummer + + if "vertegenwoordigde" in validated_data: + if vertegenwoordigde := validated_data.pop("vertegenwoordigde", []): + partijen = [str(partij["uuid"]) for partij in vertegenwoordigde] + vertegenwoordigde = Partij.objects.filter(uuid__in=partijen) + + instance.vertegenwoordigde.set(vertegenwoordigde) + partij = super().update(instance, validated_data) if partij_identificatie: @@ -627,6 +722,7 @@ def update(self, instance, validated_data): def create(self, validated_data): partij_identificatie = validated_data.pop("partij_identificatie") digitale_adressen = validated_data.pop("digitaaladres_set") + rekeningnummers = validated_data.pop("rekeningnummer_set") if voorkeurs_digitaal_adres := validated_data.pop( "voorkeurs_digitaal_adres", None @@ -646,7 +742,32 @@ def create(self, validated_data): uuid=str(voorkeurs_digitaal_adres_uuid) ) + if voorkeurs_rekeningnummer := validated_data.pop( + "voorkeurs_rekeningnummer", None + ): + voorkeurs_rekeningnummer_uuid = voorkeurs_rekeningnummer.get("uuid") + if voorkeurs_rekeningnummer and voorkeurs_rekeningnummer_uuid not in [ + rekeningnummer["uuid"] for rekeningnummer in rekeningnummers + ]: + raise serializers.ValidationError( + { + "voorkeurs_rekeningnummer": _( + "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn." + ) + } + ) + voorkeurs_rekeningnummer = Rekeningnummer.objects.get( + uuid=str(voorkeurs_rekeningnummer_uuid) + ) + + if vertegenwoordigde := validated_data.pop("vertegenwoordigde", None): + partijen = [str(partij["uuid"]) for partij in vertegenwoordigde] + validated_data["vertegenwoordigde"] = Partij.objects.filter( + uuid__in=partijen + ) + validated_data["voorkeurs_digitaal_adres"] = voorkeurs_digitaal_adres + validated_data["voorkeurs_rekeningnummer"] = voorkeurs_rekeningnummer partij = super().create(validated_data) @@ -671,6 +792,14 @@ def create(self, validated_data): digitaal_adres.partij = partij digitaal_adres.save() + if rekeningnummers: + for rekeningnummer in rekeningnummers: + rekeningnummer = Rekeningnummer.objects.get( + uuid=str(rekeningnummer["uuid"]) + ) + rekeningnummer.partij = partij + rekeningnummer.save() + return partij diff --git a/src/openklant/components/klantinteracties/api/serializers/rekeningnummers.py b/src/openklant/components/klantinteracties/api/serializers/rekeningnummers.py new file mode 100644 index 00000000..4bf3487d --- /dev/null +++ b/src/openklant/components/klantinteracties/api/serializers/rekeningnummers.py @@ -0,0 +1,70 @@ +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + +from openklant.components.klantinteracties.api.validators import Rekeningnummer_exists +from openklant.components.klantinteracties.models.partijen import Partij +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer + + +class RekeningnummerForeignKeySerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Rekeningnummer + fields = ( + "uuid", + "url", + ) + extra_kwargs = { + "uuid": {"required": True, "validators": [Rekeningnummer_exists]}, + "url": { + "view_name": "klantinteracties:rekeningnummer-detail", + "lookup_field": "uuid", + "help_text": _( + "De unieke URL van deze rekeningnummer binnen deze API." + ), + }, + } + + +class RekeningnummerSerializer(serializers.HyperlinkedModelSerializer): + from openklant.components.klantinteracties.api.serializers.partijen import ( + PartijForeignKeySerializer, + ) + + partij = PartijForeignKeySerializer( + required=True, + allow_null=True, + help_text=_("Rekeningnummer van een partij"), + ) + + class Meta: + model = Rekeningnummer + fields = ("uuid", "partij", "iban", "bic") + extra_kwargs = { + "uuid": {"read_only": True}, + "url": { + "view_name": "klantinteracties:rekeningnummer-detail", + "lookup_field": "uuid", + "help_text": _( + "De unieke URL van deze rekeningnummer binnen deze API." + ), + }, + } + + @transaction.atomic + def create(self, validated_data): + if partij := validated_data.pop("partij", None): + validated_data["partij"] = Partij.objects.get(uuid=str(partij.get("uuid"))) + + return super().create(validated_data) + + @transaction.atomic + def update(self, instance, validated_data): + if "partij" in validated_data: + if partij := validated_data.pop("partij", None): + partij = Partij.objects.get(uuid=str(partij.get("uuid"))) + + validated_data["partij"] = partij + + return super().update(instance, validated_data) diff --git a/src/openklant/components/klantinteracties/api/tests/test_partijen.py b/src/openklant/components/klantinteracties/api/tests/test_partijen.py index 56b2cbfa..c1ff7fed 100644 --- a/src/openklant/components/klantinteracties/api/tests/test_partijen.py +++ b/src/openklant/components/klantinteracties/api/tests/test_partijen.py @@ -16,6 +16,9 @@ PersoonFactory, VertegenwoordigdenFactory, ) +from openklant.components.klantinteracties.models.tests.factories.rekeningnummer import ( + RekeningnummerFactory, +) from openklant.components.token.tests.api_testcase import APITestCase @@ -75,12 +78,15 @@ def test_read_partij(self): def test_create_partij(self): digitaal_adres, digitaal_adres2 = DigitaalAdresFactory.create_batch(2) + rekeningnummer, rekeningnummer2 = RekeningnummerFactory.create_batch(2) list_url = reverse("klantinteracties:partij-list") data = { "nummer": "1298329191", "interneNotitie": "interneNotitie", "digitaleAdressen": [{"uuid": str(digitaal_adres.uuid)}], "voorkeursDigitaalAdres": {"uuid": str(digitaal_adres.uuid)}, + "rekeningnummers": [{"uuid": str(rekeningnummer.uuid)}], + "voorkeursRekeningnummer": {"uuid": str(rekeningnummer.uuid)}, "soortPartij": "persoon", "indicatieGeheimhouding": True, "voorkeurstaal": "ndl", @@ -121,6 +127,10 @@ def test_create_partij(self): self.assertEqual( data["voorkeursDigitaalAdres"]["uuid"], str(digitaal_adres.uuid) ) + self.assertEqual(data["rekeningnummers"][0]["uuid"], str(rekeningnummer.uuid)) + self.assertEqual( + data["voorkeursRekeningnummer"]["uuid"], str(rekeningnummer.uuid) + ) self.assertEqual(data["soortPartij"], "persoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -162,6 +172,8 @@ def test_create_partij(self): data["nummer"] = "1298329192" data["digitaleAdressen"] = [] data["voorkeursDigitaalAdres"] = None + data["rekeningnummers"] = [] + data["voorkeursRekeningnummer"] = None response = self.client.post(list_url, data) @@ -173,6 +185,8 @@ def test_create_partij(self): self.assertEqual(response_data["interneNotitie"], "interneNotitie") self.assertEqual(response_data["digitaleAdressen"], []) self.assertIsNone(response_data["voorkeursDigitaalAdres"]) + self.assertEqual(response_data["rekeningnummers"], []) + self.assertIsNone(response_data["voorkeursRekeningnummer"]) self.assertEqual(response_data["soortPartij"], "persoon") self.assertTrue(response_data["indicatieGeheimhouding"]) self.assertEqual(response_data["voorkeurstaal"], "ndl") @@ -260,6 +274,27 @@ def test_create_partij(self): ], ) + with self.subTest("voorkeurs_adres_must_be_given_digitaal_adres_validation"): + data["nummer"] = "1298329194" + # change voorkeursDigitaalAdres because of previous subtest + data["voorkeursDigitaalAdres"] = None + + data["voorkeursRekeningnummer"] = {"uuid": str(rekeningnummer2.uuid)} + response = self.client.post(list_url, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + response_data = response.json() + self.assertEqual( + response_data["invalidParams"], + [ + { + "name": "voorkeursRekeningnummer", + "code": "invalid", + "reason": "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn.", + } + ], + ) + def test_create_persoon(self): list_url = reverse("klantinteracties:partij-list") data = { @@ -267,6 +302,8 @@ def test_create_persoon(self): "interneNotitie": "interneNotitie", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "indicatieGeheimhouding": True, "voorkeurstaal": "ndl", "indicatieActief": True, @@ -305,6 +342,8 @@ def test_create_persoon(self): self.assertEqual(response_data["interneNotitie"], "interneNotitie") self.assertEqual(response_data["digitaleAdressen"], []) self.assertIsNone(response_data["voorkeursDigitaalAdres"]) + self.assertEqual(response_data["rekeningnummers"], []) + self.assertIsNone(response_data["voorkeursRekeningnummer"]) self.assertEqual(response_data["soortPartij"], "persoon") self.assertTrue(response_data["indicatieGeheimhouding"]) self.assertEqual(response_data["voorkeurstaal"], "ndl") @@ -355,6 +394,8 @@ def test_create_organisatie(self): "interneNotitie": "interneNotitie", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "indicatieGeheimhouding": True, "voorkeurstaal": "ndl", "indicatieActief": True, @@ -389,6 +430,8 @@ def test_create_organisatie(self): self.assertEqual(response_data["interneNotitie"], "interneNotitie") self.assertEqual(response_data["digitaleAdressen"], []) self.assertIsNone(response_data["voorkeursDigitaalAdres"]) + self.assertEqual(response_data["rekeningnummers"], []) + self.assertIsNone(response_data["voorkeursRekeningnummer"]) self.assertEqual(response_data["soortPartij"], "organisatie") self.assertTrue(response_data["indicatieGeheimhouding"]) self.assertEqual(response_data["voorkeurstaal"], "ndl") @@ -426,6 +469,8 @@ def test_create_contactpersoon(self): "interneNotitie": "interneNotitie", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "indicatieGeheimhouding": True, "voorkeurstaal": "ndl", "indicatieActief": True, @@ -467,6 +512,8 @@ def test_create_contactpersoon(self): self.assertEqual(response_data["interneNotitie"], "interneNotitie") self.assertEqual(response_data["digitaleAdressen"], []) self.assertIsNone(response_data["voorkeursDigitaalAdres"]) + self.assertEqual(response_data["rekeningnummers"], []) + self.assertIsNone(response_data["voorkeursRekeningnummer"]) self.assertEqual(response_data["soortPartij"], "contactpersoon") self.assertTrue(response_data["indicatieGeheimhouding"]) self.assertEqual(response_data["voorkeurstaal"], "ndl") @@ -513,6 +560,7 @@ def test_update_partij(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="persoon", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -539,6 +587,9 @@ def test_update_partij(self): digitaal_adres = DigitaalAdresFactory.create(partij=partij) digitaal_adres2 = DigitaalAdresFactory.create() + rekeningnummer = RekeningnummerFactory.create(partij=partij) + rekeningnummer2 = RekeningnummerFactory.create() + detail_url = reverse( "klantinteracties:partij-detail", kwargs={"uuid": str(partij.uuid)} ) @@ -556,7 +607,17 @@ def test_update_partij(self): }, ], ) + self.assertEqual( + data["rekeningnummers"], + [ + { + "uuid": str(rekeningnummer.uuid), + "url": f"http://testserver/klantinteracties/api/v1/rekeningnummers/{str(rekeningnummer.uuid)}", + }, + ], + ) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "persoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -599,6 +660,8 @@ def test_update_partij(self): "interneNotitie": "changed", "digitaleAdressen": [{"uuid": str(digitaal_adres2.uuid)}], "voorkeursDigitaalAdres": {"uuid": str(digitaal_adres2.uuid)}, + "rekeningnummers": [{"uuid": str(rekeningnummer2.uuid)}], + "voorkeursRekeningnummer": {"uuid": str(rekeningnummer2.uuid)}, "soortPartij": "persoon", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -642,9 +705,21 @@ def test_update_partij(self): }, ], ) + self.assertEqual( + data["rekeningnummers"], + [ + { + "uuid": str(rekeningnummer2.uuid), + "url": f"http://testserver/klantinteracties/api/v1/rekeningnummers/{str(rekeningnummer2.uuid)}", + }, + ], + ) self.assertEqual( data["voorkeursDigitaalAdres"]["uuid"], str(digitaal_adres2.uuid) ) + self.assertEqual( + data["voorkeursRekeningnummer"]["uuid"], str(rekeningnummer2.uuid) + ) self.assertEqual(data["soortPartij"], "persoon") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -718,12 +793,53 @@ def test_update_partij(self): ], ) + with self.subTest( + "test_voorkeurs_rekeningnummer_must_be_part_of_rekeningnummers" + ): + # set voorkeursDigitaalAdres to null because of previous subtests + data["voorkeursDigitaalAdres"] = None + + data["voorkeursRekeningnummer"] = {"uuid": str(rekeningnummer.uuid)} + response = self.client.put(detail_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + response_data = response.json() + self.assertEqual( + response_data["invalidParams"], + [ + { + "name": "voorkeursRekeningnummer", + "code": "invalid", + "reason": "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn.", + } + ], + ) + + with self.subTest( + "test_rekeningnummer_can_only_be_given_with_none_empty_rekeningnummer" + ): + data["rekeningnummers"] = [] + response = self.client.put(detail_url, data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + response_data = response.json() + self.assertEqual( + response_data["invalidParams"], + [ + { + "name": "voorkeursRekeningnummer", + "code": "invalid", + "reason": "voorkeursRekeningnummer mag niet meegegeven worden als rekeningnummers leeg is.", + } + ], + ) + with self.subTest("set_foreignkey_fields_to_none"): data = { "nummer": "6427834668", "interneNotitie": "changed", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "soortPartij": "organisatie", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -752,6 +868,8 @@ def test_update_partij(self): self.assertEqual(data["interneNotitie"], "changed") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "organisatie") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -782,6 +900,7 @@ def test_update_partij_persoon(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="persoon", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -814,6 +933,8 @@ def test_update_partij_persoon(self): self.assertEqual(data["interneNotitie"], "interneNotitie") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "persoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -856,6 +977,8 @@ def test_update_partij_persoon(self): "interneNotitie": "changed", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "soortPartij": "persoon", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -892,6 +1015,8 @@ def test_update_partij_persoon(self): self.assertEqual(data["interneNotitie"], "changed") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "persoon") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -934,6 +1059,7 @@ def test_update_partij_organisatie(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="organisatie", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -960,6 +1086,8 @@ def test_update_partij_organisatie(self): self.assertEqual(data["interneNotitie"], "interneNotitie") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "organisatie") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -991,6 +1119,8 @@ def test_update_partij_organisatie(self): "interneNotitie": "changed", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "soortPartij": "organisatie", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -1022,6 +1152,8 @@ def test_update_partij_organisatie(self): self.assertEqual(data["interneNotitie"], "changed") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "organisatie") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -1056,6 +1188,7 @@ def test_update_partij_contactpersoon(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="contactpersoon", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -1098,6 +1231,8 @@ def test_update_partij_contactpersoon(self): self.assertEqual(data["interneNotitie"], "interneNotitie") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "contactpersoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -1144,6 +1279,8 @@ def test_update_partij_contactpersoon(self): "interneNotitie": "changed", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "soortPartij": "contactpersoon", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -1181,6 +1318,8 @@ def test_update_partij_contactpersoon(self): self.assertEqual(data["interneNotitie"], "changed") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "contactpersoon") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -1227,6 +1366,7 @@ def test_update_partij_contactpersoon_to_persoon(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="contactpersoon", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -1261,6 +1401,8 @@ def test_update_partij_contactpersoon_to_persoon(self): self.assertEqual(data["interneNotitie"], "interneNotitie") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "contactpersoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -1307,6 +1449,8 @@ def test_update_partij_contactpersoon_to_persoon(self): "interneNotitie": "changed", "digitaleAdressen": [], "voorkeursDigitaalAdres": None, + "rekeningnummers": [], + "voorkeursRekeningnummer": None, "soortPartij": "persoon", "indicatieGeheimhouding": False, "voorkeurstaal": "ger", @@ -1343,6 +1487,8 @@ def test_update_partij_contactpersoon_to_persoon(self): self.assertEqual(data["interneNotitie"], "changed") self.assertEqual(data["digitaleAdressen"], []) self.assertIsNone(data["voorkeursDigitaalAdres"]) + self.assertEqual(data["rekeningnummers"], []) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertEqual(data["soortPartij"], "persoon") self.assertFalse(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ger") @@ -1385,6 +1531,7 @@ def test_partial_update_parij(self): nummer="1298329191", interne_notitie="interneNotitie", voorkeurs_digitaal_adres=None, + voorkeurs_rekeningnummer=None, soort_partij="persoon", indicatie_geheimhouding=True, voorkeurstaal="ndl", @@ -1402,6 +1549,10 @@ def test_partial_update_parij(self): ) digitaal_adres = DigitaalAdresFactory.create(partij=partij) digitaal_adres2 = DigitaalAdresFactory.create(partij=None) + + rekeningnummer = RekeningnummerFactory.create(partij=partij) + rekeningnummer2 = RekeningnummerFactory.create() + PersoonFactory.create( partij=partij, contactnaam_voorletters="P", @@ -1420,7 +1571,8 @@ def test_partial_update_parij(self): self.assertEqual(data["interneNotitie"], "interneNotitie") self.assertEqual(data["digitaleAdressen"][0]["uuid"], str(digitaal_adres.uuid)) self.assertIsNone(data["voorkeursDigitaalAdres"]) - self.assertEqual(data["soortPartij"], "persoon") + self.assertEqual(data["rekeningnummers"][0]["uuid"], str(rekeningnummer.uuid)) + self.assertIsNone(data["voorkeursRekeningnummer"]) self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") self.assertTrue(data["indicatieActief"]) @@ -1459,6 +1611,7 @@ def test_partial_update_parij(self): data = { "voorkeursDigitaalAdres": {"uuid": str(digitaal_adres.uuid)}, + "voorkeursRekeningnummer": {"uuid": str(rekeningnummer.uuid)}, "soortPartij": "persoon", } @@ -1472,6 +1625,10 @@ def test_partial_update_parij(self): self.assertEqual( data["voorkeursDigitaalAdres"]["uuid"], str(digitaal_adres.uuid) ) + self.assertEqual(data["rekeningnummers"][0]["uuid"], str(rekeningnummer.uuid)) + self.assertEqual( + data["voorkeursRekeningnummer"]["uuid"], str(rekeningnummer.uuid) + ) self.assertEqual(data["soortPartij"], "persoon") self.assertTrue(data["indicatieGeheimhouding"]) self.assertEqual(data["voorkeurstaal"], "ndl") @@ -1526,6 +1683,28 @@ def test_partial_update_parij(self): ], ) + with self.subTest( + "voorkeurs_rekeningnummer_must_be_given_rekeningnummers_validation" + ): + # set voorkeursDigitaalAdres to none because of previous subtest + data["voorkeursDigitaalAdres"] = None + + data["voorkeursRekeningnummer"] = {"uuid": str(rekeningnummer2.uuid)} + response = self.client.patch(detail_url, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + response_data = response.json() + self.assertEqual( + response_data["invalidParams"], + [ + { + "name": "voorkeursRekeningnummer", + "code": "invalid", + "reason": "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn.", + } + ], + ) + def test_destroy_partij(self): partij = PartijFactory.create() detail_url = reverse( diff --git a/src/openklant/components/klantinteracties/api/tests/test_rekeningnummers.py b/src/openklant/components/klantinteracties/api/tests/test_rekeningnummers.py new file mode 100644 index 00000000..edb9271c --- /dev/null +++ b/src/openklant/components/klantinteracties/api/tests/test_rekeningnummers.py @@ -0,0 +1,161 @@ +from rest_framework import status +from vng_api_common.tests import reverse + +from openklant.components.klantinteracties.models.tests.factories.partijen import ( + PartijFactory, +) +from openklant.components.klantinteracties.models.tests.factories.rekeningnummer import ( + RekeningnummerFactory, +) +from openklant.components.token.tests.api_testcase import APITestCase + + +class RekeningnummerTests(APITestCase): + def test_list_rekeningnummer(self): + list_url = reverse("klantinteracties:rekeningnummer-list") + RekeningnummerFactory.create_batch(2) + + response = self.client.get(list_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + self.assertEqual(len(data["results"]), 2) + + def test_read_rekeningnummer(self): + rekeningnummer = RekeningnummerFactory.create() + detail_url = reverse( + "klantinteracties:rekeningnummer-detail", + kwargs={"uuid": str(rekeningnummer.uuid)}, + ) + + response = self.client.get(detail_url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_rekeningnummer(self): + list_url = reverse("klantinteracties:rekeningnummer-list") + data = { + "partij": None, + "iban": "NL18BANK23481326", + "bic": "1734723742", + } + + response = self.client.post(list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = response.json() + + self.assertIsNone(data["partij"]) + self.assertEqual(data["iban"], "NL18BANK23481326") + self.assertEqual(data["bic"], "1734723742") + + with self.subTest("with_partij"): + partij = PartijFactory.create() + data["partij"] = {"uuid": str(partij.uuid)} + + response = self.client.post(list_url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = response.json() + + self.assertEqual(data["partij"]["uuid"], str(partij.uuid)) + self.assertEqual(data["iban"], "NL18BANK23481326") + self.assertEqual(data["bic"], "1734723742") + + def test_update_rekeningnummer(self): + partij, partij2 = PartijFactory.create_batch(2) + digitaal_adres = RekeningnummerFactory.create( + partij=partij2, + iban="NL18BANK23481326", + bic="1734723742", + ) + detail_url = reverse( + "klantinteracties:rekeningnummer-detail", + kwargs={"uuid": str(digitaal_adres.uuid)}, + ) + response = self.client.get(detail_url) + data = response.json() + + self.assertEqual(data["partij"]["uuid"], str(partij2.uuid)) + self.assertEqual(data["iban"], "NL18BANK23481326") + self.assertEqual(data["bic"], "1734723742") + + data = { + "partij": {"uuid": str(partij.uuid)}, + "iban": "NL18BANK746328229", + "bic": "8258243823", + } + + response = self.client.put(detail_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertEqual(data["partij"]["uuid"], str(partij.uuid)) + self.assertEqual(data["iban"], "NL18BANK746328229") + self.assertEqual(data["bic"], "8258243823") + + with self.subTest("update_partij_to_none"): + data = { + "partij": None, + "iban": "NL18BANK746328229", + "bic": "8258243823", + } + + response = self.client.put(detail_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertIsNone(data["partij"]) + self.assertEqual(data["iban"], "NL18BANK746328229") + self.assertEqual(data["bic"], "8258243823") + + def test_partial_update_rekeningnummer(self): + partij = PartijFactory.create() + digitaal_adres = RekeningnummerFactory.create( + partij=partij, + iban="NL18BANK23481326", + bic="1734723742", + ) + detail_url = reverse( + "klantinteracties:rekeningnummer-detail", + kwargs={"uuid": str(digitaal_adres.uuid)}, + ) + response = self.client.get(detail_url) + data = response.json() + + self.assertEqual(data["partij"]["uuid"], str(partij.uuid)) + self.assertEqual(data["iban"], "NL18BANK23481326") + self.assertEqual(data["bic"], "1734723742") + + data = { + "bic": "8438538453", + } + + response = self.client.patch(detail_url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertEqual(data["partij"]["uuid"], str(partij.uuid)) + self.assertEqual(data["iban"], "NL18BANK23481326") + self.assertEqual(data["bic"], "8438538453") + + def test_destroy_rekeningnummer(self): + rekeningnummer = RekeningnummerFactory.create() + detail_url = reverse( + "klantinteracties:rekeningnummer-detail", + kwargs={"uuid": str(rekeningnummer.uuid)}, + ) + response = self.client.delete(detail_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + list_url = reverse("klantinteracties:rekeningnummer-list") + response = self.client.get(list_url) + data = response.json() + self.assertEqual(data["count"], 0) diff --git a/src/openklant/components/klantinteracties/api/urls.py b/src/openklant/components/klantinteracties/api/urls.py index 2087831d..ca919525 100644 --- a/src/openklant/components/klantinteracties/api/urls.py +++ b/src/openklant/components/klantinteracties/api/urls.py @@ -28,6 +28,9 @@ PartijViewSet, VertegenwoordigdenViewSet, ) +from openklant.components.klantinteracties.api.viewsets.rekeningnummers import ( + RekeningnummerViewSet, +) from .schema import custom_settings @@ -38,6 +41,8 @@ router.register("digitaleadressen", DigitaalAdresViewSet) +router.register("rekeningnummers", RekeningnummerViewSet) + router.register("actorklantcontacten", ActorKlantcontactViewSet) router.register("klantcontacten", KlantcontactViewSet) router.register("betrokkenen", BetrokkeneViewSet) diff --git a/src/openklant/components/klantinteracties/api/validators.py b/src/openklant/components/klantinteracties/api/validators.py index 9035eb99..c7103bc3 100644 --- a/src/openklant/components/klantinteracties/api/validators.py +++ b/src/openklant/components/klantinteracties/api/validators.py @@ -22,6 +22,7 @@ Partij, PartijIdentificator, ) +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer class FKUniqueTogetherValidator(UniqueTogetherValidator): @@ -158,3 +159,10 @@ def partij_identificator_exists(value): PartijIdentificator.objects.get(uuid=str(value)) except PartijIdentificator.DoesNotExist: raise serializers.ValidationError(_("PartijIdentificator object bestaat niet.")) + + +def Rekeningnummer_exists(value): + try: + Rekeningnummer.objects.get(uuid=str(value)) + except Rekeningnummer.DoesNotExist: + raise serializers.ValidationError(_("Rekeningnummer object bestaat niet.")) diff --git a/src/openklant/components/klantinteracties/api/viewsets/rekeningnummers.py b/src/openklant/components/klantinteracties/api/viewsets/rekeningnummers.py new file mode 100644 index 00000000..82cbfe8d --- /dev/null +++ b/src/openklant/components/klantinteracties/api/viewsets/rekeningnummers.py @@ -0,0 +1,55 @@ +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import viewsets +from rest_framework.pagination import PageNumberPagination + +from openklant.components.klantinteracties.api.serializers.rekeningnummers import ( + RekeningnummerSerializer, +) +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer +from openklant.components.token.authentication import TokenAuthentication +from openklant.components.token.permission import TokenPermissions + + +@extend_schema(tags=["rekeningnummers"]) +@extend_schema_view( + list=extend_schema( + summary="Alle rekeningnummers opvragen.", + description="Alle rekeningnummers opvragen.", + ), + retrieve=extend_schema( + summary="Een specifiek rekeningnummer opvragen.", + description="Een specifiek rekeningnummer opvragen.", + ), + create=extend_schema( + summary="Maak een rekeningnummer aan.", + description="Maak een rekeningnummer aan.", + ), + update=extend_schema( + summary="Werk een rekeningnummer in zijn geheel bij.", + description="Werk een rekeningnummer in zijn geheel bij.", + ), + partial_update=extend_schema( + summary="Werk een rekeningnummer deels bij.", + description="Werk een rekeningnummer deels bij.", + ), + destroy=extend_schema( + summary="Verwijder een rekeningnummer.", + description="Verwijder een rekeningnummer.", + ), +) +class RekeningnummerViewSet(viewsets.ModelViewSet): + queryset = Rekeningnummer.objects.order_by("-pk").select_related( + "partij", + ) + serializer_class = RekeningnummerSerializer + lookup_field = "uuid" + pagination_class = PageNumberPagination + filter_backends = [DjangoFilterBackend] + filterset_fields = [ + "uuid", + "iban", + "bic", + ] + authentication_classes = (TokenAuthentication,) + permission_classes = (TokenPermissions,) diff --git a/src/openklant/components/klantinteracties/migrations/0011_rekeningnummer.py b/src/openklant/components/klantinteracties/migrations/0011_rekeningnummer.py new file mode 100644 index 00000000..f940b3b1 --- /dev/null +++ b/src/openklant/components/klantinteracties/migrations/0011_rekeningnummer.py @@ -0,0 +1,108 @@ +# Generated by Django 3.2.23 on 2024-01-31 17:33 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import openklant.utils.validators +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("klantinteracties", "0010_auto_20240207_1416"), + ] + + operations = [ + migrations.AlterField( + model_name="partij", + name="voorkeurs_digitaal_adres", + field=models.ForeignKey( + blank=True, + help_text="'Partij' gaf voorkeur aan voor contact via 'Digitaal adres'", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="voorkeurs_partij", + to="klantinteracties.digitaaladres", + verbose_name="voorkeurs digitaal adres", + ), + ), + migrations.CreateModel( + name="Rekeningnummer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + help_text="Unieke (technische) identificatiecode van de interne taak.", + unique=True, + ), + ), + ( + "iban", + models.CharField( + help_text="Het internationaal bankrekeningnummer, zoals dat door een bankinstelling als identificator aan een overeenkomst tussen de bank en een of meer subjecten wordt toegekend, op basis waarvan het SUBJECT in de regel internationaal financieel communiceert.", + max_length=34, + validators=[ + openklant.utils.validators.CustomRegexValidator( + message="Ongeldige IBAN", + regex="^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]{1,30}$", + ) + ], + verbose_name="IBAN", + ), + ), + ( + "bic", + models.CharField( + blank=True, + help_text="De unieke code van de bankinstelling waar het SUBJECT het bankrekeningnummer heeft waarmee het subject in de regel internationaal financieel communiceert.", + max_length=11, + validators=[ + django.core.validators.MinLengthValidator(8), + openklant.utils.validators.CustomRegexValidator( + message="Geen spaties toegestaan", regex="^[^\\S]+$" + ), + ], + verbose_name="BIC", + ), + ), + ( + "partij", + models.ForeignKey( + blank=True, + help_text="'Rekeninnummer' van 'Partij'", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="klantinteracties.partij", + verbose_name="partij", + ), + ), + ], + options={ + "verbose_name": "rekeningnummer", + "verbose_name_plural": "rekeningnummers", + }, + ), + migrations.AddField( + model_name="partij", + name="voorkeurs_rekeningnummer", + field=models.ForeignKey( + blank=True, + help_text="'Partij' gaf voorkeur voor 'rekeningnummer'", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="voorkeurs_rekeningnummer", + to="klantinteracties.rekeningnummer", + verbose_name="voorkeurs rekeningnummer", + ), + ), + ] diff --git a/src/openklant/components/klantinteracties/models/__init__.py b/src/openklant/components/klantinteracties/models/__init__.py index 90a38d8c..b4c99a52 100644 --- a/src/openklant/components/klantinteracties/models/__init__.py +++ b/src/openklant/components/klantinteracties/models/__init__.py @@ -3,3 +3,4 @@ from .internetaken import * # noqa from .klantcontacten import * # noqa from .partijen import * # noqa +from .rekeningnummers import * # noqa diff --git a/src/openklant/components/klantinteracties/models/partijen.py b/src/openklant/components/klantinteracties/models/partijen.py index ceb49e1e..79f946ba 100644 --- a/src/openklant/components/klantinteracties/models/partijen.py +++ b/src/openklant/components/klantinteracties/models/partijen.py @@ -29,6 +29,15 @@ class Partij(APIMixin, BezoekadresMixin, CorrespondentieadresMixin): null=True, blank=True, ) + voorkeurs_rekeningnummer = models.ForeignKey( + "klantinteracties.Rekeningnummer", + on_delete=models.CASCADE, + related_name="voorkeurs_rekeningnummer", + verbose_name=_("voorkeurs rekeningnummer"), + help_text=_("'Partij' gaf voorkeur voor 'rekeningnummer'"), + null=True, + blank=True, + ) nummer = models.CharField( _("nummer"), help_text=_( @@ -94,6 +103,14 @@ def clean(self): _("Het voorkeurs adres moet een gelinkte digitaal adres zijn.") ) + if self.voorkeurs_rekeningnummer: + if self.voorkeurs_rekeningnummer not in self.rekeningnummer_set.all(): + raise ValidationError( + _( + "Het voorkeurs rekeningnummer moet een gelinkte rekeningnummer zijn." + ) + ) + def save(self, *args, **kwargs): number_generator(self, Partij) return super().save(*args, **kwargs) diff --git a/src/openklant/components/klantinteracties/models/rekeningnummers.py b/src/openklant/components/klantinteracties/models/rekeningnummers.py new file mode 100644 index 00000000..9b6fe7aa --- /dev/null +++ b/src/openklant/components/klantinteracties/models/rekeningnummers.py @@ -0,0 +1,51 @@ +import uuid + +from django.core.validators import MinLengthValidator +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from openklant.utils.validators import validate_iban, validate_no_space + + +class Rekeningnummer(models.Model): + uuid = models.UUIDField( + unique=True, + default=uuid.uuid4, + help_text=_("Unieke (technische) identificatiecode van de interne taak."), + ) + partij = models.ForeignKey( + "klantinteracties.Partij", + on_delete=models.CASCADE, + verbose_name=_("partij"), + help_text=_("'Rekeninnummer' van 'Partij'"), + null=True, + blank=True, + ) + iban = models.CharField( + _("IBAN"), + help_text=_( + "Het internationaal bankrekeningnummer, zoals dat door een bankinstelling als " + "identificator aan een overeenkomst tussen de bank en een of meer subjecten wordt " + "toegekend, op basis waarvan het SUBJECT in de regel internationaal financieel communiceert." + ), + max_length=34, + validators=[validate_iban], + blank=False, + ) + bic = models.CharField( + _("BIC"), + help_text=_( + "De unieke code van de bankinstelling waar het SUBJECT het bankrekeningnummer " + "heeft waarmee het subject in de regel internationaal financieel communiceert." + ), + max_length=11, + validators=[MinLengthValidator(8), validate_no_space], + blank=True, + ) + + class Meta: + verbose_name = _("rekeningnummer") + verbose_name_plural = _("rekeningnummers") + + def __str__(self): + return self.iban diff --git a/src/openklant/components/klantinteracties/models/tests/factories/rekeningnummer.py b/src/openklant/components/klantinteracties/models/tests/factories/rekeningnummer.py new file mode 100644 index 00000000..0330d2ba --- /dev/null +++ b/src/openklant/components/klantinteracties/models/tests/factories/rekeningnummer.py @@ -0,0 +1,12 @@ +import factory + +from openklant.components.klantinteracties.models.rekeningnummers import Rekeningnummer + + +class RekeningnummerFactory(factory.django.DjangoModelFactory): + uuid = factory.Faker("uuid4") + iban = factory.Sequence(lambda n: f"NL18{n}") + bic = factory.Sequence(lambda n: f"1234567{n}") + + class Meta: + model = Rekeningnummer diff --git a/src/openklant/components/klantinteracties/openapi.yaml b/src/openklant/components/klantinteracties/openapi.yaml index aed8b1fe..2c684b95 100644 --- a/src/openklant/components/klantinteracties/openapi.yaml +++ b/src/openklant/components/klantinteracties/openapi.yaml @@ -2138,6 +2138,163 @@ paths: responses: '204': description: No response body + /rekeningnummers: + get: + operationId: rekeningnummersList + description: Alle rekeningnummers opvragen. + summary: Alle rekeningnummers opvragen. + parameters: + - in: query + name: bic + schema: + type: string + - in: query + name: iban + schema: + type: string + - name: page + required: false + in: query + description: Een pagina binnen de gepagineerde set resultaten. + schema: + type: integer + - in: query + name: uuid + schema: + type: string + format: uuid + tags: + - rekeningnummers + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedRekeningnummerList' + description: '' + post: + operationId: rekeningnummersCreate + description: Maak een rekeningnummer aan. + summary: Maak een rekeningnummer aan. + tags: + - rekeningnummers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + required: true + security: + - tokenAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + description: '' + /rekeningnummers/{uuid}: + get: + operationId: rekeningnummersRetrieve + description: Een specifiek rekeningnummer opvragen. + summary: Een specifiek rekeningnummer opvragen. + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unieke (technische) identificatiecode van de interne taak. + required: true + tags: + - rekeningnummers + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + description: '' + put: + operationId: rekeningnummersUpdate + description: Werk een rekeningnummer in zijn geheel bij. + summary: Werk een rekeningnummer in zijn geheel bij. + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unieke (technische) identificatiecode van de interne taak. + required: true + tags: + - rekeningnummers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + description: '' + patch: + operationId: rekeningnummersPartialUpdate + description: Werk een rekeningnummer deels bij. + summary: Werk een rekeningnummer deels bij. + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unieke (technische) identificatiecode van de interne taak. + required: true + tags: + - rekeningnummers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedRekeningnummer' + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Rekeningnummer' + description: '' + delete: + operationId: rekeningnummersDestroy + description: Verwijder een rekeningnummer. + summary: Verwijder een rekeningnummer. + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + description: Unieke (technische) identificatiecode van de interne taak. + required: true + tags: + - rekeningnummers + security: + - tokenAuth: [] + responses: + '204': + description: No response body /vertegenwoordigingen: get: operationId: vertegenwoordigingenList @@ -3438,6 +3595,26 @@ components: type: array items: $ref: '#/components/schemas/Partij' + PaginatedRekeningnummerList: + type: object + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=4 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?page=2 + results: + type: array + items: + $ref: '#/components/schemas/Rekeningnummer' PaginatedVertegenwoordigdenList: type: object properties: @@ -3503,8 +3680,8 @@ components: items: $ref: '#/components/schemas/DigitaalAdresForeignKey' nullable: true - description: Digitaal adres dat een partij verstrekte voor gebruik bij toekomstig - contact met de gemeente. + description: Digitaal adresen dat een partij verstrekte voor gebruik bij + toekomstig contact met de gemeente. voorkeursDigitaalAdres: allOf: - $ref: '#/components/schemas/DigitaalAdresForeignKey' @@ -3517,6 +3694,17 @@ components: $ref: '#/components/schemas/PartijForeignKey' readOnly: true description: Partijen die een andere partijen vertegenwoordigden. + rekeningnummers: + type: array + items: + $ref: '#/components/schemas/RekeningnummerForeignKey' + nullable: true + description: Rekeningnummers van een partij + voorkeursRekeningnummer: + allOf: + - $ref: '#/components/schemas/RekeningnummerForeignKey' + nullable: true + description: Rekeningsnummer die een partij bij voorkeur gebruikt. partijIdentificatoren: type: array items: @@ -3567,11 +3755,13 @@ components: - indicatieActief - indicatieGeheimhouding - partijIdentificatoren + - rekeningnummers - soortPartij - url - uuid - vertegenwoordigden - voorkeursDigitaalAdres + - voorkeursRekeningnummer PartijBezoekadres: type: object description: |- @@ -4243,8 +4433,8 @@ components: items: $ref: '#/components/schemas/DigitaalAdresForeignKey' nullable: true - description: Digitaal adres dat een partij verstrekte voor gebruik bij toekomstig - contact met de gemeente. + description: Digitaal adresen dat een partij verstrekte voor gebruik bij + toekomstig contact met de gemeente. voorkeursDigitaalAdres: allOf: - $ref: '#/components/schemas/DigitaalAdresForeignKey' @@ -4257,6 +4447,17 @@ components: $ref: '#/components/schemas/PartijForeignKey' readOnly: true description: Partijen die een andere partijen vertegenwoordigden. + rekeningnummers: + type: array + items: + $ref: '#/components/schemas/RekeningnummerForeignKey' + nullable: true + description: Rekeningnummers van een partij + voorkeursRekeningnummer: + allOf: + - $ref: '#/components/schemas/RekeningnummerForeignKey' + nullable: true + description: Rekeningsnummer die een partij bij voorkeur gebruikt. partijIdentificatoren: type: array items: @@ -4334,6 +4535,34 @@ components: nullable: true description: Gegevens die een partij in een basisregistratie of ander extern register uniek identificeren. + PatchedRekeningnummer: + type: object + properties: + uuid: + type: string + format: uuid + readOnly: true + description: Unieke (technische) identificatiecode van de interne taak. + partij: + allOf: + - $ref: '#/components/schemas/PartijForeignKey' + nullable: true + description: Rekeningnummer van een partij + iban: + type: string + description: Het internationaal bankrekeningnummer, zoals dat door een bankinstelling + als identificator aan een overeenkomst tussen de bank en een of meer subjecten + wordt toegekend, op basis waarvan het SUBJECT in de regel internationaal + financieel communiceert. + pattern: ^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]{1,30}$ + maxLength: 34 + bic: + type: string + description: De unieke code van de bankinstelling waar het SUBJECT het bankrekeningnummer + heeft waarmee het subject in de regel internationaal financieel communiceert. + pattern: ^[\S]+$ + maxLength: 11 + minLength: 8 PatchedVertegenwoordigden: type: object properties: @@ -4355,6 +4584,53 @@ components: allOf: - $ref: '#/components/schemas/PartijForeignKey' description: '''Partij'' vertegenwoordigd wordt door een andere ''Partij''.' + Rekeningnummer: + type: object + properties: + uuid: + type: string + format: uuid + readOnly: true + description: Unieke (technische) identificatiecode van de interne taak. + partij: + allOf: + - $ref: '#/components/schemas/PartijForeignKey' + nullable: true + description: Rekeningnummer van een partij + iban: + type: string + description: Het internationaal bankrekeningnummer, zoals dat door een bankinstelling + als identificator aan een overeenkomst tussen de bank en een of meer subjecten + wordt toegekend, op basis waarvan het SUBJECT in de regel internationaal + financieel communiceert. + pattern: ^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]{1,30}$ + maxLength: 34 + bic: + type: string + description: De unieke code van de bankinstelling waar het SUBJECT het bankrekeningnummer + heeft waarmee het subject in de regel internationaal financieel communiceert. + pattern: ^[\S]+$ + maxLength: 11 + minLength: 8 + required: + - iban + - partij + - uuid + RekeningnummerForeignKey: + type: object + properties: + uuid: + type: string + format: uuid + description: Unieke (technische) identificatiecode van de interne taak. + url: + type: string + format: uri + readOnly: true + description: De unieke URL van deze rekeningnummer binnen deze API. + required: + - url + - uuid RolEnum: enum: - vertegenwoordiger diff --git a/src/openklant/utils/tests/test_validators.py b/src/openklant/utils/tests/test_validators.py index 672eaf2f..6cf6f77a 100644 --- a/src/openklant/utils/tests/test_validators.py +++ b/src/openklant/utils/tests/test_validators.py @@ -3,6 +3,8 @@ from openklant.utils.validators import ( validate_charfield_entry, + validate_iban, + validate_no_space, validate_phone_number, validate_postal_code, ) @@ -91,3 +93,44 @@ def test_validate_phone_number(self): self.assertEqual(validate_phone_number("00695959595"), "00695959595") self.assertEqual(validate_phone_number("00-69-59-59-59-5"), "00-69-59-59-59-5") self.assertEqual(validate_phone_number("00 69 59 59 59 5"), "00 69 59 59 59 5") + + def test_validate_no_space_validator(self): + invalid_strings = [ + "aaaa aaaa", + " bbbbbbbb", + "cccccccc ", + "d d d d d", + ] + + for invalid_string in invalid_strings: + self.assertRaisesMessage( + ValidationError, + "Geen spaties toegestaan", + validate_no_space, + invalid_string, + ) + + self.assertIsNone(validate_no_space("nospaces")) + + def test_validate_iban(self): + invalid_ibans = [ + "1231md4832842834", + "jda42034nnndnd23923", + "AB123dasd#asdasda", + "AB12", + "AB1259345934953495934953495345345345", + ] + + for invalid_iban in invalid_ibans: + self.assertRaisesMessage( + ValidationError, + "Ongeldige IBAN", + validate_iban, + invalid_iban, + ) + + self.assertIsNone(validate_iban("AB12TEST1253678")) + self.assertIsNone(validate_iban("AB12test1253678")) + self.assertIsNone(validate_iban("ab1299999999999")) + self.assertIsNone(validate_iban("ab129")) + self.assertIsNone(validate_iban("ab12aaaaaaaaaa")) diff --git a/src/openklant/utils/validators.py b/src/openklant/utils/validators.py index d68fc19c..69898a9c 100644 --- a/src/openklant/utils/validators.py +++ b/src/openklant/utils/validators.py @@ -49,3 +49,12 @@ def __call__(self, value): validate_postal_code = CustomRegexValidator( regex="^[1-9][0-9]{3} ?[a-zA-Z]{2}$", message=_("Ongeldige postcode") ) + +# Doesn't truely validate if IBAN is valid but validated the basic pattern. +validate_iban = CustomRegexValidator( + regex="^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]{1,30}$", message=_("Ongeldige IBAN") +) + +validate_no_space = CustomRegexValidator( + regex="^[\S]+$", message=_("Geen spaties toegestaan") # noqa +)