diff --git a/src/open_inwoner/accounts/views/contactmoments.py b/src/open_inwoner/accounts/views/contactmoments.py index b8aa6cfedc..45b9af0dab 100644 --- a/src/open_inwoner/accounts/views/contactmoments.py +++ b/src/open_inwoner/accounts/views/contactmoments.py @@ -13,9 +13,11 @@ from view_breadcrumbs import BaseBreadcrumbMixin from open_inwoner.accounts.models import User +from open_inwoner.openklant.constants import KlantenServiceType from open_inwoner.openklant.models import KlantContactMomentAnswer from open_inwoner.openklant.services import ( KlantenService, + OpenKlant2Service, Question, QuestionValidator, ZaakWithApiGroup, @@ -63,7 +65,7 @@ class VragenService(Protocol): def list_questions( self, fetch_params: FetchParameters, - user: User, + user: User | None = None, ) -> Iterable[Question]: # noqa: E704 ... @@ -87,9 +89,11 @@ def get_fetch_parameters( class KlantContactMomentBaseView( CommonPageMixin, BaseBreadcrumbMixin, KlantContactMomentAccessMixin, TemplateView ): - def get_service(self) -> VragenService: - # TODO: Refactor to support both OpenKlant2 and eSuite services at once - return eSuiteVragenService() + def get_service(self, service_type: str) -> VragenService: + if service_type == KlantenServiceType.ESUITE: + return eSuiteVragenService() + elif service_type == KlantenServiceType.OPENKLANT2: + return OpenKlant2Service() def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) @@ -131,13 +135,21 @@ def get_anchors(self) -> list: def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - service = self.get_service() - questions = service.list_questions( - self.get_fetch_params(service), user=self.request.user + esuite_service = self.get_service(service_type=KlantenServiceType.ESUITE) + ok2_service = self.get_service(service_type=KlantenServiceType.OPENKLANT2) + + questions_esuite = esuite_service.list_questions( + fetch_params=self.get_fetch_params(esuite_service), + user=self.request.user, ) - ctx["contactmomenten"] = [ - QuestionValidator.validate_python(q) for q in questions - ] + questions_ok2 = ok2_service.list_questions( + self.get_fetch_params(ok2_service), + user=self.request.user, + ) + all_questions = questions_esuite + questions_ok2 + all_questions.sort(key=lambda q: q["registered_date"], reverse=True) + ctx["contactmomenten"] = all_questions + paginator_dict = self.paginate_with_context(ctx["contactmomenten"]) ctx.update(paginator_dict) @@ -162,7 +174,7 @@ def get_anchors(self) -> list: def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - service = self.get_service() + service = self.get_service(service_type=KlantenServiceType.ESUITE) kcm, zaak = service.retrieve_question( self.get_fetch_params(service), kwargs["kcm_uuid"], user=self.request.user diff --git a/src/open_inwoner/openklant/constants.py b/src/open_inwoner/openklant/constants.py index b90d290ac6..32be46e42d 100644 --- a/src/open_inwoner/openklant/constants.py +++ b/src/open_inwoner/openklant/constants.py @@ -1,3 +1,5 @@ +import enum + from django.db import models from django.utils.translation import gettext_lazy as _ @@ -17,3 +19,8 @@ def safe_label(cls, value, default=""): if default: return default return str(value).replace("_", " ").title() + + +class KlantenServiceType(enum.Enum): + ESUITE = "esuite" + OPENKLANT2 = "openklant2" diff --git a/src/open_inwoner/openklant/models.py b/src/open_inwoner/openklant/models.py index e32310311e..c1d0029910 100644 --- a/src/open_inwoner/openklant/models.py +++ b/src/open_inwoner/openklant/models.py @@ -1,7 +1,7 @@ import uuid from dataclasses import dataclass from typing import Self -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse, urlunparse from django.core.exceptions import ImproperlyConfigured from django.db import models @@ -207,7 +207,10 @@ class OpenKlant2Config: @property def api_url(self): - return urljoin(self.api_root, self.api_path) + joined = urljoin(self.api_root, self.api_path) + scheme, netloc, path, params, query, fragment = urlparse(joined) + path = path.replace("//", "/") + return urlunparse((scheme, netloc, path, params, query, fragment)) @classmethod def from_django_settings(cls) -> Self: diff --git a/src/open_inwoner/openklant/services.py b/src/open_inwoner/openklant/services.py index 3a8b4ffc59..9d7575347a 100644 --- a/src/open_inwoner/openklant/services.py +++ b/src/open_inwoner/openklant/services.py @@ -1,8 +1,10 @@ import datetime import logging import uuid +from datetime import timedelta from typing import Iterable, Literal, NotRequired, Protocol, Self +from django.conf import settings from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -10,7 +12,7 @@ import glom from ape_pie.client import APIClient from attr import dataclass -from pydantic import TypeAdapter +from pydantic import BaseModel, ConfigDict, TypeAdapter from requests.exceptions import RequestException from typing_extensions import TypedDict from zgw_consumers.api_models.base import factory @@ -31,7 +33,7 @@ KlantCreateData, ObjectContactMoment, ) -from open_inwoner.openklant.constants import Status +from open_inwoner.openklant.constants import KlantenServiceType, Status from open_inwoner.openklant.models import ( ContactFormSubject, KlantContactMomentAnswer, @@ -49,6 +51,7 @@ from open_inwoner.openzaak.models import ZGWApiGroupConfig from open_inwoner.utils.api import ClientError, get_json_response from open_inwoner.utils.logentry import system_action +from open_inwoner.utils.time import instance_is_new from openklant2.client import OpenKlant2Client from openklant2.types.resources.digitaal_adres import DigitaalAdres from openklant2.types.resources.klant_contact import ( @@ -80,15 +83,17 @@ class ZaakWithApiGroup: class Question(TypedDict): identification: str + source_url: str # points to contactmoment or klantcontact subject: str registered_date: datetime.datetime question_text: str answer_text: str | None status: str - - new_answer_available: bool - case_detail_url: str channel: str + case_detail_url: str | None = None + + api_service: KlantenServiceType + new_answer_available: bool = False QuestionValidator = TypeAdapter(Question) @@ -593,7 +598,7 @@ def _get_question_data( if isinstance(kcm.contactmoment, str): raise ValueError("Received unresolved contactmoment") - return { + question_data = { "answer_text": kcm.contactmoment.antwoord, "identification": kcm.contactmoment.identificatie, "question_text": kcm.contactmoment.tekst, @@ -607,7 +612,10 @@ def _get_question_data( "cases:contactmoment_detail", kwargs={"kcm_uuid": kcm.uuid} ), "channel": kcm.contactmoment.kanaal.title(), + "source_url": kcm.contactmoment.url, + "api_service": KlantenServiceType.ESUITE, } + return QuestionValidator.validate_python(question_data) def fetch_klantcontactmomenten( self, @@ -700,6 +708,9 @@ class OpenKlant2Answer: nummer: str plaatsgevonden_op: datetime.datetime + # metadata + viewed: bool = False + @classmethod def from_klantcontact(cls, klantcontact: KlantContact) -> Self: if klantcontact["inhoud"] is None: @@ -716,14 +727,20 @@ def from_klantcontact(cls, klantcontact: KlantContact) -> Self: @dataclass(frozen=True) -class OpenKlant2Question: +class OpenKlant2Question(BaseModel): + url: str question: str question_kcm_uuid: str + onderwerp: str + kanaal: str + taal: str nummer: str plaatsgevonden_op: datetime.datetime answer: OpenKlant2Answer | None = None + model_config = ConfigDict(arbitrary_types_allowed=True) + @classmethod def from_klantcontact_and_answer( cls, klantcontact: KlantContact, answer: OpenKlant2Answer | None = None @@ -734,11 +751,15 @@ def from_klantcontact_and_answer( return cls( question=klantcontact["inhoud"], question_kcm_uuid=klantcontact["uuid"], + onderwerp=klantcontact["onderwerp"], + kanaal=klantcontact["kanaal"], + taal=klantcontact["taal"], nummer=klantcontact["nummer"], plaatsgevonden_op=datetime.datetime.fromisoformat( klantcontact["plaatsgevondenOp"] ), answer=answer, + url=klantcontact["url"], ) @@ -1144,6 +1165,10 @@ def create_answer( def klantcontacten_for_partij( self, partij_uuid: str, *, kanaal: str | None = None ) -> Iterable[KlantContact]: + # There is currently no good way to filter the klantcontacten by a + # Partij (see https://github.com/maykinmedia/open-klant/issues/256). So + # unfortunately, we have to fetch all rows and do the filtering client + # side. params: ListKlantContactParams = { "expand": [ "leiddeTotInterneTaken", @@ -1151,28 +1176,9 @@ def klantcontacten_for_partij( "hadBetrokkenen", "hadBetrokkenen.wasPartij", ], + "kanaal": kanaal or self.config.mijn_vragen_kanaal, } - if kanaal: - params["kanaal"] = kanaal - klantcontacten = self.client.klant_contact.list_iter(params=params) - - # There is currently no good way to filter the klantcontacten by a - # Partij (see https://github.com/maykinmedia/open-klant/issues/256). So - # unfortunately, we have to fetch all rows and do the filtering client - # side. - klantcontacten = self.client.klant_contact.list_iter( - params={ - "expand": [ - "leiddeTotInterneTaken", - "gingOverOnderwerpobjecten", - "hadBetrokkenen", - "hadBetrokkenen.wasPartij", - ], - "kanaal": self.config.mijn_vragen_kanaal, - } - ) - klantcontacten_for_partij = filter( lambda row: partij_uuid in glom.glom( @@ -1237,3 +1243,55 @@ def questions_for_partij(self, partij_uuid: str) -> list[OpenKlant2Question]: question_objs.sort(key=lambda o: o.plaatsgevonden_op) return question_objs + + def list_questions( + self, fetch_params: FetchParameters, user: User + ) -> list[Question]: + if bsn := fetch_params.get("user_bsn"): + partij = self.find_persoon_for_bsn(bsn) + elif kvk_or_rsin := fetch_params.get("user_kvk_or_rsin"): + partij = self.find_organisatie_for_kvk(kvk_or_rsin) + + questions = self.questions_for_partij(partij_uuid=partij["uuid"]) + return self._reformat_questions(questions, user) + + # TODO: handle `status` + `new_answer_available` + # `case_detail_url`: will be handled in integration of detail view + # `status`: eSuite has three: "nieuw", "in behandeling", "afgehandeld" + def _reformat_questions( + self, + questions_ok2: list[OpenKlant2Question], + user: User, + ) -> list[Question]: + questions = [] + for q in questions_ok2: + answer_metadata = KlantContactMomentAnswer.objects.get_or_create( + user=user, contactmoment_url=q.url + ) + question = { + "identification": q.nummer, + "source_url": q.url, + "subject": q.onderwerp, + "registered_date": q.plaatsgevonden_op, + "question_text": q.question, + "answer_text": q.answer.answer, + "status": "", + "channel": q.kanaal, + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": self._has_new_answer_available( + q, answer=answer_metadata + ), + } + questions.append(question) + return [QuestionValidator.validate_python(q) for q in questions] + + def _has_new_answer_available( + self, question: Question, answer: KlantContactMomentAnswer + ) -> bool: + answer_is_recent = instance_is_new( + question.answer, + "plaatsgevonden_op", + timedelta(days=settings.CONTACTMOMENT_NEW_DAYS), + ) + return answer_is_recent and not answer.is_seen diff --git a/src/open_inwoner/openklant/tests/test_esuite_vragen_service.py b/src/open_inwoner/openklant/tests/test_esuite_vragen_service.py index 7d0fc7e7f2..d2a096bb96 100644 --- a/src/open_inwoner/openklant/tests/test_esuite_vragen_service.py +++ b/src/open_inwoner/openklant/tests/test_esuite_vragen_service.py @@ -6,7 +6,7 @@ import requests_mock from open_inwoner.accounts.tests.factories import UserFactory -from open_inwoner.openklant.constants import Status +from open_inwoner.openklant.constants import KlantenServiceType, Status from open_inwoner.openklant.models import ContactFormSubject, OpenKlantConfig from open_inwoner.openklant.services import eSuiteVragenService from open_inwoner.openklant.tests.data import MockAPIReadData @@ -89,16 +89,18 @@ def test_list_questions_returns_expected_rows(self, m): self.assertEqual( questions[0], { + "identification": expected_contactmoment["identificatie"], + "source_url": expected_contactmoment["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( expected_contactmoment["registratiedatum"] ), - "channel": expected_contactmoment["kanaal"].title(), "question_text": expected_contactmoment["tekst"], - "subject": self.contactformsubject.subject, "answer_text": expected_contactmoment["antwoord"], - "identification": expected_contactmoment["identificatie"], "status": str(Status.afgehandeld.label), + "channel": expected_contactmoment["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @@ -162,16 +164,18 @@ def test_retrieve_question_returns_expected_result(self, m): self.assertEqual( question, { + "identification": expected_contactmoment["identificatie"], + "source_url": expected_contactmoment["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( expected_contactmoment["registratiedatum"] ), - "channel": expected_contactmoment["kanaal"].title(), "question_text": expected_contactmoment["tekst"], - "subject": self.contactformsubject.subject, "answer_text": expected_contactmoment["antwoord"], - "identification": expected_contactmoment["identificatie"], "status": str(Status.afgehandeld.label), + "channel": expected_contactmoment["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) diff --git a/src/open_inwoner/openklant/tests/test_openklant2_service.py b/src/open_inwoner/openklant/tests/test_openklant2_service.py index e6be707604..646a8d8623 100644 --- a/src/open_inwoner/openklant/tests/test_openklant2_service.py +++ b/src/open_inwoner/openklant/tests/test_openklant2_service.py @@ -416,10 +416,14 @@ def test_create_question(self): self.assertEqual( question, OpenKlant2Question( + url=klantcontact["url"], answer=None, nummer=klantcontact["nummer"], question_kcm_uuid=klantcontact["uuid"], question="A question asked by Alice", + onderwerp="Important questions", + kanaal=self.openklant2_config.mijn_vragen_kanaal, + taal="nld", plaatsgevonden_op=QUESTION_DATE, ), ) diff --git a/src/open_inwoner/openklant/tests/test_views.py b/src/open_inwoner/openklant/tests/test_views.py index b145080113..5725fa2add 100644 --- a/src/open_inwoner/openklant/tests/test_views.py +++ b/src/open_inwoner/openklant/tests/test_views.py @@ -15,7 +15,7 @@ from open_inwoner.accounts.tests.factories import UserFactory from open_inwoner.configurations.models import SiteConfiguration from open_inwoner.openklant.api_models import ContactMoment, Klant, KlantContactMoment -from open_inwoner.openklant.constants import Status +from open_inwoner.openklant.constants import KlantenServiceType, Status from open_inwoner.openklant.models import ( ContactFormSubject, KlantContactMomentAnswer, @@ -34,8 +34,52 @@ from .factories import KlantContactMomentAnswerFactory +class MockOpenKlant2Service: + def list_questions(self, fetch_params={}, user=None): + return [ + { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + } + ] + + def retrieve_question(self, fetch_params={}, question_uuid="", user=None): + return ( + { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + None, + ) + + def get_fetch_parameters(self, request=None, user=None, use_vestigingsnummer=False): + return {"user_bsn": "123456789"} + + @requests_mock.Mocker() @patch.object(eSuiteVragenService, "get_kcm_answer_mapping", autospec=True) +@patch( + "open_inwoner.accounts.views.contactmoments.OpenKlant2Service", + return_value=MockOpenKlant2Service(), +) @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") @modify_settings( MIDDLEWARE={"remove": ["open_inwoner.kvk.middleware.KvKLoginMiddleware"]} @@ -61,7 +105,9 @@ def setUp(self): config=klanten_config, ) - def test_list_for_bsn(self, m, mock_get_kcm_answer_mapping): + def test_list_for_bsn( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) # make sure internal contactmoment is present in data (should be excluded from kcms in view) @@ -77,18 +123,36 @@ def test_list_for_bsn(self, m, mock_get_kcm_answer_mapping): kcms = response.context["contactmomenten"] cm_data = data.contactmoment - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @@ -103,11 +167,14 @@ def test_list_for_bsn(self, m, mock_get_kcm_answer_mapping): status_item = response.pyquery.find(f"p:contains('{_('Status')}')").parent() - self.assertEqual(status_item.text(), f"{_('Status')}\n{_('Afgehandeld')}") + self.assertIn(f"{_('Status')}\n{_('Onbekend')}", status_item.text()) + self.assertIn(f"{_('Status')}\n{_('Afgehandeld')}", status_item.text()) self.assertNotIn(_("Nieuw antwoord beschikbaar"), response.text) @freeze_time("2022-01-01") - def test_list_for_bsn_new_answer_available(self, m, mock_get_kcm_answer_mapping): + def test_list_for_bsn_new_answer_available( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) # make sure internal contactmoment is present in data (should be excluded from kcms in view) @@ -133,18 +200,36 @@ def test_list_for_bsn_new_answer_available(self, m, mock_get_kcm_answer_mapping) kcms = response.context["contactmomenten"] cm_data = data.contactmoment - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": True, }, ) @@ -159,10 +244,13 @@ def test_list_for_bsn_new_answer_available(self, m, mock_get_kcm_answer_mapping) status_item = response.pyquery.find(f"p:contains('{_('Status')}')").parent() - self.assertEqual(status_item.text(), f"{_('Status')}\n{_('Afgehandeld')}") + self.assertIn(f"{_('Status')}\n{_('Onbekend')}", status_item.text()) + self.assertIn(f"{_('Status')}\n{_('Afgehandeld')}", status_item.text()) self.assertIn(_("Nieuw antwoord beschikbaar"), response.text) - def test_list_for_kvk_or_rsin(self, m, mock_get_kcm_answer_mapping): + def test_list_for_kvk_or_rsin( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): for use_rsin_for_innNnpId_query_parameter in [True, False]: with self.subTest( use_rsin_for_innNnpId_query_parameter=use_rsin_for_innNnpId_query_parameter @@ -186,28 +274,49 @@ def test_list_for_kvk_or_rsin(self, m, mock_get_kcm_answer_mapping): kcms = response.context["contactmomenten"] cm_data = data.contactmoment2 - registratiedatum = datetime.fromisoformat(cm_data["registratiedatum"]) - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat( + "2024-01-01T12:00:00Z" + ), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( cm_data["registratiedatum"] ), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @set_kvk_branch_number_in_session("1234") - def test_list_for_vestiging(self, m, mock_get_kcm_answer_mapping): + def test_list_for_vestiging( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) self.client.force_login(user=data.eherkenning_user) @@ -233,27 +342,48 @@ def test_list_for_vestiging(self, m, mock_get_kcm_answer_mapping): kcms = response.context["contactmomenten"] cm_data = data.contactmoment_vestiging - registratiedatum = datetime.fromisoformat(cm_data["registratiedatum"]) - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat( + "2024-01-01T12:00:00Z" + ), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( cm_data["registratiedatum"] ), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) - def test_disable_contactmoment_form(self, m, mock_get_kcm_answer_mapping): + def test_disable_contactmoment_form( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) list_url = reverse("cases:contactmoment_list") @@ -273,7 +403,9 @@ def test_disable_contactmoment_form(self, m, mock_get_kcm_answer_mapping): contactform = doc.find("[data-testid='contactmomenten__contact_form']") self.assertEqual(contactform, []) - def test_show_detail_for_bsn(self, m, mock_get_kcm_answer_mapping): + def test_show_detail_for_bsn( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) detail_url = reverse( @@ -289,19 +421,23 @@ def test_show_detail_for_bsn(self, m, mock_get_kcm_answer_mapping): self.assertEqual( kcm, { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) - def test_show_detail_for_bsn_with_zaak(self, m, mock_get_kcm_answer_mapping): + def test_show_detail_for_bsn_with_zaak( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m, link_objectcontactmomenten=True) detail_url = reverse( @@ -319,14 +455,16 @@ def test_show_detail_for_bsn_with_zaak(self, m, mock_get_kcm_answer_mapping): self.assertEqual( kcm, { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @@ -360,7 +498,7 @@ def test_show_detail_for_bsn_with_zaak(self, m, mock_get_kcm_answer_mapping): self.assertEqual(kcm_local.is_seen, True) def test_show_detail_for_bsn_with_zaak_reformat_esuite_id( - self, m, mock_get_kcm_answer_mapping + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping ): data = MockAPIReadData().install_mocks(m, link_objectcontactmomenten=True) @@ -382,14 +520,16 @@ def test_show_detail_for_bsn_with_zaak_reformat_esuite_id( self.assertEqual( kcm, { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @@ -415,7 +555,7 @@ def test_show_detail_for_bsn_with_zaak_reformat_esuite_id( ) def test_display_contactmoment_subject_duplicate_esuite_codes( - self, m, mock_get_kcm_answer_mapping + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping ): """ Assert that the first OIP subject is used if several are mapped to the same e-suite code @@ -438,24 +578,42 @@ def test_display_contactmoment_subject_duplicate_esuite_codes( kcms = response.context["contactmomenten"] cm_data = data.contactmoment - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) def test_display_contactmoment_subject_no_mapping_fallback( - self, m, mock_get_kcm_answer_mapping + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping ): """ Assert that the e-suite subject code is displayed if no mapping is configured in OIP @@ -474,23 +632,43 @@ def test_display_contactmoment_subject_no_mapping_fallback( kcms = response.context["contactmomenten"] cm_data = data.contactmoment - self.assertEqual(len(kcms), 1) + self.assertEqual(len(kcms), 2) self.assertEqual( kcms[0], { + "identification": "openklant2_identification", + "source_url": "http://www.openklant2/test/url", + "subject": "openklan2_subject", + "registered_date": datetime.fromisoformat("2024-01-01T12:00:00Z"), + "question_text": "hello?", + "answer_text": "no", + "status": "Onbekend", + "channel": "email", + "case_detail_url": "", + "api_service": KlantenServiceType.OPENKLANT2, + "new_answer_available": False, + }, + ) + self.assertEqual( + kcms[1], + { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject_code, "registered_date": datetime.fromisoformat(cm_data["registratiedatum"]), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject_code, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) - def test_show_detail_for_kvk_or_rsin(self, m, mock_get_kcm_answer_mapping): + def test_show_detail_for_kvk_or_rsin( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): for use_rsin_for_innNnpId_query_parameter in [True, False]: with self.subTest( use_rsin_for_innNnpId_query_parameter=use_rsin_for_innNnpId_query_parameter @@ -520,22 +698,26 @@ def test_show_detail_for_kvk_or_rsin(self, m, mock_get_kcm_answer_mapping): self.assertEqual( kcm, { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( cm_data["registratiedatum"] ), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @set_kvk_branch_number_in_session("1234") - def test_show_detail_for_vestiging(self, m, mock_get_kcm_answer_mapping): + def test_show_detail_for_vestiging( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): data = MockAPIReadData().install_mocks(m) self.client.force_login(user=data.eherkenning_user) @@ -561,23 +743,25 @@ def test_show_detail_for_vestiging(self, m, mock_get_kcm_answer_mapping): self.assertEqual( kcm, { + "identification": cm_data["identificatie"], + "source_url": cm_data["url"], + "subject": self.contactformsubject.subject, "registered_date": datetime.fromisoformat( cm_data["registratiedatum"] ), - "channel": cm_data["kanaal"].title(), "question_text": cm_data["tekst"], - "subject": self.contactformsubject.subject, "answer_text": cm_data["antwoord"], - "identification": cm_data["identificatie"], "status": str(Status.afgehandeld.label), + "channel": cm_data["kanaal"].title(), "case_detail_url": detail_url, + "api_service": KlantenServiceType.ESUITE, "new_answer_available": False, }, ) @set_kvk_branch_number_in_session("1234") def test_cannot_access_detail_for_hoofdvestiging_as_vestiging( - self, m, mock_get_kcm_answer_mapping + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping ): data = MockAPIReadData().install_mocks(m) self.client.force_login(user=data.eherkenning_user) @@ -602,24 +786,32 @@ def test_cannot_access_detail_for_hoofdvestiging_as_vestiging( self.assertEqual(response.status_code, 404) - def test_list_requires_bsn_or_kvk(self, m, mock_get_kcm_answer_mapping): + def test_list_requires_bsn_or_kvk( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): user = UserFactory() list_url = reverse("cases:contactmoment_list") response = self.app.get(list_url, user=user) self.assertRedirects(response, reverse("pages-root")) - def test_list_requires_login(self, m, mock_get_kcm_answer_mapping): + def test_list_requires_login( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): list_url = reverse("cases:contactmoment_list") response = self.app.get(list_url) self.assertRedirects(response, f"{reverse('login')}?next={list_url}") - def test_detail_requires_bsn_or_kvk(self, m, mock_get_kcm_answer_mapping): + def test_detail_requires_bsn_or_kvk( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): user = UserFactory() url = reverse("cases:contactmoment_detail", kwargs={"kcm_uuid": uuid4()}) response = self.app.get(url, user=user) self.assertRedirects(response, reverse("pages-root")) - def test_detail_requires_login(self, m, mock_get_kcm_answer_mapping): + def test_detail_requires_login( + self, m, mock_openklant2_service, mock_get_kcm_answer_mapping + ): url = reverse("cases:contactmoment_detail", kwargs={"kcm_uuid": uuid4()}) response = self.app.get(url) self.assertRedirects(response, f"{reverse('login')}?next={url}") diff --git a/src/open_inwoner/templates/pages/contactmoment/list.html b/src/open_inwoner/templates/pages/contactmoment/list.html index ee64e3ab0e..376f866513 100644 --- a/src/open_inwoner/templates/pages/contactmoment/list.html +++ b/src/open_inwoner/templates/pages/contactmoment/list.html @@ -26,7 +26,7 @@