Skip to content

Commit

Permalink
Merge pull request #1553 from maykinmedia/2959-sync-user-to-esuite-po…
Browse files Browse the repository at this point in the history
…st-registration

[#2959] Sync user to eSuite after completing registration
  • Loading branch information
alextreme authored Jan 15, 2025
2 parents 65b1058 + 72f8e8a commit 520107a
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 49 deletions.
34 changes: 1 addition & 33 deletions src/open_inwoner/accounts/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging

from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -37,6 +36,7 @@ def update_user_on_login(sender, user, request, *args, **kwargs):
if user.login_type not in [LoginTypeChoices.digid, LoginTypeChoices.eherkenning]:
return

# KvK API
if user.login_type is LoginTypeChoices.eherkenning:
_update_eherkenning_user_from_kvk_api(user=user)

Expand Down Expand Up @@ -89,38 +89,6 @@ def _update_eherkenning_user_from_kvk_api(user: User):
user.save()


# TODO: Should we also try to fetch pre-existing klant for new user and update?
# The klant could have been created by a different service.
@receiver(post_save, sender=User)
def get_or_create_klant_for_new_user(
sender: type, instance: User, created: bool, **kwargs
) -> None:
if not created:
logger.info("No klanten sync performed because user has just been created")
return

user = instance

# OpenKlant2
try:
service = OpenKlant2Service()
except Exception:
logger.error("OpenKlant2 service failed to build")
else:
_update_user_from_openklant2(
user,
service,
)

# eSuite
try:
service = eSuiteKlantenService()
except Exception:
logger.error("eSuiteKlantenService failed to build")
else:
_update_user_from_esuite(user, service)


@receiver(user_logged_in)
def log_user_login(sender, user, request, *args, **kwargs):
current_path = request.path
Expand Down
70 changes: 69 additions & 1 deletion src/open_inwoner/accounts/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def test_notification_settings_with_cms_page_published(self, m):
necessary_form[
"case_notification_channel"
] = NotificationChannelChoice.digital_only
necessary_form["email"] = "[email protected]"
necessary_form.submit()

user = User.objects.get(bsn=data["auth_name"])
Expand All @@ -304,12 +305,13 @@ def test_notification_settings_with_cms_page_published(self, m):
self.assertEqual(
klant_patch_data,
{
"emailadres": "[email protected]",
"toestemmingZaakNotificatiesAlleenDigitaal": True,
},
)
# only check logs for klant api update
dump = self.getTimelineLogDump()
msg = "patched klant from user profile edit with fields: toestemmingZaakNotificatiesAlleenDigitaal"
msg = "patched klant from user profile edit with fields: emailadres, toestemmingZaakNotificatiesAlleenDigitaal"
assert msg in dump

@requests_mock.Mocker()
Expand Down Expand Up @@ -507,6 +509,72 @@ def test_existing_user_digid_login_fails_brp_update_when_brp_http_404(self, m):

self.assertNotEqual(user.first_name, "UpdatedName")

@requests_mock.Mocker()
def test_user_without_klant_is_created_and_updated(self, m):
"""
Assert that if no klant exists during the filling of the necessary fields form,
the klant is created and updated with the writable fields (though not with
the toestemmingZaakNotificatiesAlleenDigitaal by default, which is tested
above).
"""
MockAPIReadPatchData.setUpServices()
mock_api_data = MockAPIReadPatchData().install_mocks(m)

# reset noise from signals
m.reset_mock()
self.clearTimelineLogs()

invite = InviteFactory()

url = reverse("digid-mock:password")
params = {
"acs": reverse("acs"),
"next": f"{reverse('profile:registration_necessary')}?invite={invite.key}",
}
url = f"{url}?{urlencode(params)}"

data = {
"auth_name": mock_api_data.user_without_klant.bsn,
"auth_pass": "bar",
}

# post our password to the IDP
response = self.app.post(url, data).follow().follow()

necessary_form = response.forms["necessary-form"]

self.assertNotIn("cases_notifications", necessary_form.fields)
self.assertNotIn("messages_notifications", necessary_form.fields)

necessary_form["email"] = "[email protected]"
necessary_form.submit()

# klant did not exist, so is created
klant_post_data = mock_api_data.matchers[3].request_history[0].json()
self.assertEqual(
klant_post_data,
{"subjectIdentificatie": {"inpBsn": "665155311"}},
)

# klant is patched with email address
klant_patch_data = mock_api_data.matchers[4].request_history[0].json()
self.assertEqual(
klant_patch_data,
{
"emailadres": "[email protected]",
# Not enabled
# "toestemmingZaakNotificatiesAlleenDigitaal": True,
},
)

# ensure klant operations are logged
dump = self.getTimelineLogDump()
for msg in (
"created klant (87654321) for user",
"patched klant from user profile edit with fields: emailadres",
):
assert msg in dump


@override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls")
class eHerkenningRegistrationTest(AssertRedirectsMixin, WebTest):
Expand Down
45 changes: 31 additions & 14 deletions src/open_inwoner/accounts/views/registration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
from urllib.parse import unquote

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import NoReverseMatch, reverse
Expand All @@ -11,9 +13,7 @@
from django_registration.backends.one_step.views import RegistrationView
from furl import furl

from open_inwoner.accounts.choices import NotificationChannelChoice
from open_inwoner.accounts.views.mixins import KlantenAPIMixin
from open_inwoner.configurations.models import SiteConfiguration
from open_inwoner.openklant.services import eSuiteKlantenService
from open_inwoner.utils.views import CommonPageMixin, LogMixin

from ...mail.verification import send_user_email_verification_mail
Expand All @@ -22,6 +22,8 @@
from ..forms import CustomRegistrationForm, NecessaryUserForm
from ..models import Invite, OpenIDDigiDConfig, OpenIDEHerkenningConfig, User

logger = logging.getLogger(__name__)


class InviteMixin(CommonPageMixin):
def get_initial(self):
Expand Down Expand Up @@ -136,7 +138,6 @@ def get(self, request, *args, **kwargs):
class NecessaryFieldsUserView(
LogMixin,
LoginRequiredMixin,
KlantenAPIMixin,
InviteMixin,
UpdateView,
):
Expand Down Expand Up @@ -164,7 +165,7 @@ def get_form_kwargs(self):
def form_valid(self, form):
user = form.save()

self.update_klant({k: form.cleaned_data[k] for k in form.changed_data})
self.update_klant(form)

invite = form.cleaned_data["invite"]
if invite:
Expand All @@ -186,18 +187,34 @@ def get_initial(self):

return initial

def update_klant(self, user_form_data: dict):
config = SiteConfiguration.get_solo()
if not config.enable_notification_channel_choice:
def update_klant(self, form: NecessaryUserForm):
try:
service = eSuiteKlantenService()
except ImproperlyConfigured:
logger.info("Unable to build KlantenService")
return

if notification_channel := user_form_data.get("case_notification_channel"):
self.patch_klant(
update_data={
"toestemmingZaakNotificatiesAlleenDigitaal": notification_channel
== NotificationChannelChoice.digital_only
}
user = form.instance

klant, _ = service.get_or_create_klant(
user=user, fetch_params=service.get_fetch_parameters(user=user)
)

if not klant:
logger.error(
"Unable to create klant during post-registration sync",
extra={"user": user},
)
return

update_fields = ["emailadres"]
if "phonenumber" in form.cleaned_data.keys():
update_fields.append("telefoonnummer")

if "case_notification_channel" in form.cleaned_data.keys():
update_fields.append("toestemmingZaakNotificatiesAlleenDigitaal")

service.update_klant_from_user(klant, user, update_fields=update_fields)


class EmailVerificationUserView(LogMixin, LoginRequiredMixin, TemplateView):
Expand Down
47 changes: 46 additions & 1 deletion src/open_inwoner/openklant/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import uuid
from datetime import timedelta
from typing import Iterable, Literal, NotRequired, Protocol, Self
from typing import Iterable, Literal, NotRequired, Protocol, Self, Sequence

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
Expand Down Expand Up @@ -330,6 +330,51 @@ def partial_update_klant(self, klant: Klant, update_data) -> Klant | None:

return klant

def update_klant_from_user(
self,
klant: Klant,
user: User,
update_fields: (
Sequence[
Literal[
"telefoonnummer",
"emailadres",
"toestemmingZaakNotificatiesAlleenDigitaal",
]
]
| None
) = None,
):
valid_update_fields = {
"telefoonnummer",
"emailadres",
"toestemmingZaakNotificatiesAlleenDigitaal",
}

if update_fields:
for field in update_fields:
if field not in valid_update_fields:
raise ValueError(f"{field} is not a valid entry for update_fields")

update_data: KlantWritePayload = {
"emailadres": user.email,
"telefoonnummer": user.phonenumber,
"toestemmingZaakNotificatiesAlleenDigitaal": user.case_notification_channel
== NotificationChannelChoice.digital_only,
}

the_keys = set(update_data.keys())
if update_fields and set(update_fields) != valid_update_fields:
for field in the_keys:
if field not in update_fields:
del update_data[field]

self.partial_update_klant(klant, update_data)
system_action(
f"patched klant from user profile edit with fields: {', '.join(sorted(update_data.keys()))}",
user=user,
)


class eSuiteVragenService(KlantenService):
config: ESuiteKlantConfig
Expand Down
46 changes: 46 additions & 0 deletions src/open_inwoner/openklant/tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def __init__(self):
email="[email protected]",
phonenumber="0100000000",
)
self.user_without_klant = DigidUserFactory(
email="[email protected]",
phonenumber="0100000000",
bsn="665155311",
)
self.eherkenning_user = eHerkenningUserFactory(
email="[email protected]",
kvk="12345678",
Expand Down Expand Up @@ -75,6 +80,32 @@ def __init__(self):
telefoonnummer="0123456789",
toestemmingZaakNotificatiesAlleenDigitaal=False,
)
self.created_klant_bsn = generate_oas_component_cached(
"kc",
"schemas/Klant",
bronorganisatie="123456789",
klantnummer="87654321",
subjectIdentificatie={
"inpBsn": "665155311",
},
url=f"{KLANTEN_ROOT}klant/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
emailadres="[email protected]",
telefoonnummer="0199995544",
toestemmingZaakNotificatiesAlleenDigitaal=False,
)
self.created_klant_bsn_updated = generate_oas_component_cached(
"kc",
"schemas/Klant",
bronorganisatie="123456789",
klantnummer="87654321",
subjectIdentificatie={
"inpBsn": "665155311",
},
url=f"{KLANTEN_ROOT}klant/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
emailadres="[email protected]",
telefoonnummer="0199995544",
toestemmingZaakNotificatiesAlleenDigitaal=False,
)
self.klant_eherkenning_old = generate_oas_component_cached(
"kc",
"schemas/Klant",
Expand Down Expand Up @@ -109,6 +140,21 @@ def install_mocks(self, m) -> "MockAPIReadPatchData":
json=self.klant_bsn_updated,
status_code=200,
),
# Create and update flow
m.get(
f"{KLANTEN_ROOT}klanten?subjectNatuurlijkPersoon__inpBsn={self.user_without_klant.bsn}",
json=paginated_response([]),
),
m.post(
f"{KLANTEN_ROOT}klanten",
json=self.created_klant_bsn,
status_code=201,
),
m.patch(
f"{KLANTEN_ROOT}klant/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
json=self.created_klant_bsn_updated,
status_code=200,
),
]
return self

Expand Down

0 comments on commit 520107a

Please sign in to comment.