Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2737] Sync user personal data with OpenKlant2 #1396

Merged
merged 3 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions bin/record_openklant2_fixtures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
#
# This script will run all tests tagged with "openklant2" and re-record the
# underlying cassettes by dynamically spinning up a live server instance using
# the Openklant2ServiceTestCase test utility and replacing the existing
# cassettes.
#
# Run this script from the root of the repository

set -e

delete_path=$(realpath ./src/open_inwoner/openklant/tests/cassettes)

# Display the full path and ask for confirmation
echo "You are about to recursively delete all VCR cassettes from the following directory:"
echo "$delete_path"
read -p "Are you sure you want to proceed? (y/N): " confirm

if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
echo "Deleting directory..."
rm -rf "$delete_path"
echo "Directory deleted."
else
echo "Operation cancelled."
exit 0
fi

set -x
RECORD_OPENKLANT_CASSETTES=1 python src/manage.py test src/open_inwoner --tag openklant2
305 changes: 303 additions & 2 deletions src/open_inwoner/openklant/services.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging
from typing import Iterable, Literal

from open_inwoner.accounts.choices import NotificationChannelChoice
from open_inwoner.accounts.models import User
from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.openklant.api_models import Klant
from open_inwoner.openklant.clients import build_klanten_client
from open_inwoner.utils.logentry import system_action
from openklant2.client import OpenKlant2Client
from openklant2.types.resources.digitaal_adres import DigitaalAdres
from openklant2.types.resources.partij import Partij, PartijListParams

from .wrap import get_fetch_parameters
from .wrap import FetchParameters, get_fetch_parameters

logger = logging.getLogger(__name__)

Expand All @@ -16,7 +20,8 @@ def get_or_create_klant_from_request(request):
if not (client := build_klanten_client()):
return

fetch_params = get_fetch_parameters(request)
if (fetch_params := get_fetch_parameters(request)) is None:
return

if klant := client.retrieve_klant(**fetch_params):
msg = "retrieved klant for user"
Expand Down Expand Up @@ -79,3 +84,299 @@ def update_user_from_klant(klant: Klant, user: User):
f"updated user from klant API with fields: {', '.join(sorted(update_data.keys()))}",
content_object=user,
)


class OpenKlant2Service:

client: OpenKlant2Client

def __init__(self, client: OpenKlant2Client):
if not isinstance(client, OpenKlant2Client):
raise ValueError(
f"`client` must be an instance of {type(OpenKlant2Client)}"
)
self.client = client

def find_partij_for_params(self, params: PartijListParams):
resp = self.client.partij.list(params=params)

if (count := resp["count"]) > 0:
if count > 1:
# TODO: Is this a use-case? The API seems to allow this
Copy link
Member

Choose a reason for hiding this comment

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

This seems like a valid issue for OK, good that you already raised it: maykinmedia/open-klant#241

logger.error("Multiple personen found in Openklant2 for a single BSN")

return self.client.partij.retrieve(
resp["results"][0]["uuid"],
params={
"expand": [
"digitaleAdressen",
"betrokkenen",
"betrokkenen.hadKlantcontact",
]
},
)

return None

@staticmethod
def _bsn_list_param(bsn: str) -> PartijListParams:
return {
"partijIdentificator__codeSoortObjectId": "bsn",
"partijIdentificator__codeRegister": "brp",
"partijIdentificator__codeObjecttype": "inp",
"partijIdentificator__objectId": bsn,
"soortPartij": "persoon",
}

@staticmethod
def _kvk_list_param(kvk: str) -> PartijListParams:
return {
"partijIdentificator__codeSoortObjectId": "kvk",
"partijIdentificator__codeRegister": "hr",
"partijIdentificator__codeObjecttype": "nnp",
"partijIdentificator__objectId": kvk,
"soortPartij": "organisatie",
}

@staticmethod
def _vestigingsnummer_list_param(vestigingsnummer: str) -> PartijListParams:
return {
"partijIdentificator__codeSoortObjectId": "vtn",
"partijIdentificator__codeRegister": "hr",
"partijIdentificator__codeObjecttype": "nnp",
"partijIdentificator__objectId": vestigingsnummer,
"soortPartij": "organisatie",
}

def find_persoon_for_bsn(self, bsn: str) -> Partij | None:
return self.find_partij_for_params(params=self._bsn_list_param(bsn))

def find_organisatie_for_kvk(self, kvk: str) -> Partij | None:
return self.find_partij_for_params(params=self._kvk_list_param(kvk))

def find_organisatie_for_vestigingsnummer(
self, vestigingsnummer: str
) -> Partij | None:
return self.find_partij_for_params(
params=self._vestigingsnummer_list_param(vestigingsnummer)
)

def get_or_create_partij_for_user(
self, fetch_params: FetchParameters, user: User
) -> Partij | None:
partij = None
created = False

if bsn := fetch_params.get("user_bsn"):
if not (persoon := self.find_persoon_for_bsn(bsn)):
persoon = self.client.partij.create_persoon(
data={
"digitaleAdressen": None,
"voorkeursDigitaalAdres": None,
"rekeningnummers": None,
"voorkeursRekeningnummer": None,
"indicatieGeheimhouding": False,
"indicatieActief": True,
"voorkeurstaal": "nld",
"soortPartij": "persoon",
"partijIdentificatie": {
"contactnaam": {
"voornaam": user.first_name,
"achternaam": user.last_name,
"voorletters": "",
"voorvoegselAchternaam": "",
},
},
}
)
created = True

try:
self.client.partij_identificator.create(
data={
"identificeerdePartij": {"uuid": persoon["uuid"]},
"partijIdentificator": {
"codeObjecttype": "inp",
"codeSoortObjectId": "bsn",
"objectId": bsn,
"codeRegister": "brp",
},
}
)
except Exception:
logger.exception("Unable to register identificatoren for partij")

partij = persoon

elif kvk := fetch_params.get("user_kvk_or_rsin"):

# Prefer vestigingsnummer if present, to stay consistent with OK1 behavior
organisatie: Partij | None
if vestigingsnummer := fetch_params.get("vestigingsnummer"):
organisatie = self.find_organisatie_for_vestigingsnummer(
vestigingsnummer
)
else:
organisatie = self.find_organisatie_for_kvk(kvk)

if not organisatie:
organisatie = self.client.partij.create_organisatie(
data={
"digitaleAdressen": None,
"voorkeursDigitaalAdres": None,
"rekeningnummers": None,
"voorkeursRekeningnummer": None,
"indicatieGeheimhouding": False,
"indicatieActief": True,
"voorkeurstaal": "nld",
"soortPartij": "organisatie",
"partijIdentificatie": {
"naam": user.company_name,
},
}
)
created = True

for soort_object_id, object_id in (
("kvk", kvk),
("vtn", vestigingsnummer),
):
if object_id:
try:
self.client.partij_identificator.create(
data={
"identificeerdePartij": {
"uuid": organisatie["uuid"]
},
"partijIdentificator": {
"codeObjecttype": "nnp",
"codeSoortObjectId": soort_object_id,
"objectId": object_id,
"codeRegister": "hr",
},
}
)
except Exception:
logger.exception(
"Unable to register identificatoren for partij"
)

partij = organisatie

if not partij:
logger.error("Unable to create OpenKlant2 partij for user")
return

msg = (
f"{'created' if created else 'retrieved'} partij {partij['uuid']} for user"
)
system_action(msg, content_object=user)

return partij

def retrieve_digitale_addressen_for_partij(
self, partij: Partij
) -> list[DigitaalAdres]:
if expand := partij.get("_expand"):
if digitale_adressen := expand.get("digitaleAdressen"):
return digitale_adressen

expand_partij = self.client.partij.retrieve(
partij["uuid"], params={"expand": ["digitaleAdressen"]}
)

if expand := expand_partij.get("_expand"):
if digitale_adressen := expand.get("digitaleAdressen"):
return digitale_adressen

# TODO: A missing _expand can mean there are no addresses.
# See: https://github.com/maykinmedia/open-klant/issues/243
return []

def filter_digitale_addressen_for_partij(
self,
partij: Partij,
*,
soortDigitaalAdres: str,
adressen: Iterable[DigitaalAdres] | None = None,
) -> list[DigitaalAdres]:
if adressen is None:
adressen = self.retrieve_digitale_addressen_for_partij(partij)

return [
digitaal_adres
for digitaal_adres in adressen
if digitaal_adres["soortDigitaalAdres"] == soortDigitaalAdres
]

def get_or_create_digitaal_adres(
self,
partij: Partij,
soortAdres: Literal["email", "telefoon"],
adres: str,
) -> tuple[DigitaalAdres, bool]:
digitale_adressen = self.filter_digitale_addressen_for_partij(
partij, soortDigitaalAdres=soortAdres
)
for digitaal_adres in digitale_adressen:
if digitaal_adres["adres"] == adres:
return digitaal_adres, False

return (
self.client.digitaal_adres.create(
data={
"adres": adres,
"soortDigitaalAdres": soortAdres,
"verstrektDoorPartij": {
"uuid": partij["uuid"],
},
"verstrektDoorBetrokkene": None,
"omschrijving": "OIP profiel",
}
),
True,
)

def update_user_from_partij(self, partij: Partij, user: User):
update_data = {}

adressen = self.retrieve_digitale_addressen_for_partij(partij)

if email_adressen := self.filter_digitale_addressen_for_partij(
partij, soortDigitaalAdres="email", adressen=adressen
):
email = email_adressen[0]["adres"]
if not User.objects.filter(email__iexact=email).exists():
update_data["email"] = email

if phone_adressen := self.filter_digitale_addressen_for_partij(
partij, soortDigitaalAdres="telefoon", adressen=adressen
):
update_data["phonenumber"] = phone_adressen[0]["adres"]

if update_data:
for attr, value in update_data.items():
setattr(user, attr, value)
user.save(update_fields=update_data.keys())

system_action(
f"updated user from klant API with fields: {', '.join(sorted(update_data.keys()))}",
content_object=user,
)

def update_partij_from_user(self, partij: Partij, user: User):
updated_fields = []
for attr, soort_adres in (("email", "email"), ("phonenumber", "telefoon")):
_, created = self.get_or_create_digitaal_adres(
partij,
soort_adres,
getattr(user, attr),
)
if created:
updated_fields.append(f"digitaleAddresen.{soort_adres}")

if updated_fields:
system_action(
f"updated Partij from user with fields: {', '.join(sorted(updated_fields))}",
content_object=user,
)
Loading
Loading