From 6cf0101b89e9272f9572a4aa71f83a2126ce34cf Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Thu, 30 Jan 2025 12:11:39 +0200 Subject: [PATCH] Create an access code when approving a reservation without one This can happen in a denied reservation returns to handling and is then approved. Also modify return to handling mutation to not make the deactivate call if access code was removed. --- .../test_reservation/test_approve.py | 55 +++++++++++++++---- .../test_requires_handling.py | 26 +++++++++ .../serializers/approve_serializers.py | 17 ++++-- .../requires_handling_serializers.py | 3 +- tilavarauspalvelu/typing.py | 2 + 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/tests/test_graphql_api/test_reservation/test_approve.py b/tests/test_graphql_api/test_reservation/test_approve.py index 11a3412e60..38bae67de4 100644 --- a/tests/test_graphql_api/test_reservation/test_approve.py +++ b/tests/test_graphql_api/test_reservation/test_approve.py @@ -1,10 +1,13 @@ from __future__ import annotations +import datetime + import pytest from tilavarauspalvelu.enums import AccessType, ReservationStateChoice from tilavarauspalvelu.integrations.keyless_entry import PindoraClient from tilavarauspalvelu.integrations.keyless_entry.exceptions import PindoraAPIError +from utils.date_utils import DEFAULT_TIMEZONE, local_datetime from tests.factories import ReservationFactory, ReservationUnitFactory from tests.helpers import patch_method @@ -109,15 +112,14 @@ def test_reservation__approve__succeeds_with_empty_handling_details(graphql): @patch_method(PindoraClient.activate_reservation_access_code) +@patch_method(PindoraClient.create_reservation) def test_reservation__approve__succeeds__pindora_api__call_succeeds(graphql): - reservation_unit = ReservationUnitFactory.create( - access_type=AccessType.ACCESS_CODE, - ) reservation = ReservationFactory.create( + reservation_units__access_type=AccessType.ACCESS_CODE, state=ReservationStateChoice.REQUIRES_HANDLING, - reservation_units=[reservation_unit], access_type=AccessType.ACCESS_CODE, access_code_is_active=False, + access_code_generated_at=local_datetime(), ) graphql.login_with_superuser() @@ -130,19 +132,19 @@ def test_reservation__approve__succeeds__pindora_api__call_succeeds(graphql): assert reservation.state == ReservationStateChoice.CONFIRMED assert reservation.access_code_is_active is True - assert PindoraClient.activate_reservation_access_code.call_count == 1 + assert PindoraClient.activate_reservation_access_code.called is True + assert PindoraClient.create_reservation.called is False @patch_method(PindoraClient.activate_reservation_access_code, side_effect=PindoraAPIError("Error")) +@patch_method(PindoraClient.create_reservation) def test_reservation__approve__succeeds__pindora_api__call_fails(graphql): - reservation_unit = ReservationUnitFactory.create( - access_type=AccessType.ACCESS_CODE, - ) reservation = ReservationFactory.create( + reservation_units__access_type=AccessType.ACCESS_CODE, state=ReservationStateChoice.REQUIRES_HANDLING, - reservation_units=[reservation_unit], access_type=AccessType.ACCESS_CODE, access_code_is_active=False, + access_code_generated_at=local_datetime(), ) graphql.login_with_superuser() @@ -156,4 +158,37 @@ def test_reservation__approve__succeeds__pindora_api__call_fails(graphql): assert reservation.state == ReservationStateChoice.CONFIRMED assert reservation.access_code_is_active is False - assert PindoraClient.activate_reservation_access_code.call_count == 1 + assert PindoraClient.activate_reservation_access_code.called is True + assert PindoraClient.create_reservation.called is False + + +@patch_method(PindoraClient.activate_reservation_access_code) +@patch_method( + PindoraClient.create_reservation, + return_value={ + "access_code_generated_at": datetime.datetime(2023, 1, 1, tzinfo=DEFAULT_TIMEZONE), + "access_code_is_active": True, + }, +) +def test_reservation__approve__succeeds__pindora_api__create_if_not_generated(graphql): + reservation = ReservationFactory.create( + reservation_units__access_type=AccessType.ACCESS_CODE, + state=ReservationStateChoice.REQUIRES_HANDLING, + access_type=AccessType.ACCESS_CODE, + access_code_is_active=False, + access_code_generated_at=None, + ) + + graphql.login_with_superuser() + data = get_approve_data(reservation) + response = graphql(APPROVE_MUTATION, input_data=data) + + assert response.has_errors is False, response.errors + + reservation.refresh_from_db() + assert reservation.state == ReservationStateChoice.CONFIRMED + assert reservation.access_code_generated_at == datetime.datetime(2023, 1, 1, tzinfo=DEFAULT_TIMEZONE) + assert reservation.access_code_is_active is True + + assert PindoraClient.activate_reservation_access_code.called is False + assert PindoraClient.create_reservation.called is True diff --git a/tests/test_graphql_api/test_reservation/test_requires_handling.py b/tests/test_graphql_api/test_reservation/test_requires_handling.py index ad6819b121..ec770bb460 100644 --- a/tests/test_graphql_api/test_reservation/test_requires_handling.py +++ b/tests/test_graphql_api/test_reservation/test_requires_handling.py @@ -6,6 +6,7 @@ from tilavarauspalvelu.enums import AccessType, ReservationNotification, ReservationStateChoice from tilavarauspalvelu.integrations.keyless_entry import PindoraClient from tilavarauspalvelu.integrations.keyless_entry.exceptions import PindoraAPIError, PindoraNotFoundError +from utils.date_utils import local_datetime from tests.factories import ReservationFactory, UserFactory from tests.helpers import patch_method @@ -85,6 +86,7 @@ def test_reservation__requires_handling__pindora_api__call_succeeds(graphql): state=ReservationStateChoice.CONFIRMED, access_type=AccessType.ACCESS_CODE, access_code_is_active=True, + access_code_generated_at=local_datetime(), ) graphql.login_with_superuser() @@ -106,6 +108,7 @@ def test_reservation__requires_handling__pindora_api__call_fails(graphql): state=ReservationStateChoice.CONFIRMED, access_type=AccessType.ACCESS_CODE, access_code_is_active=True, + access_code_generated_at=local_datetime(), ) graphql.login_with_superuser() @@ -123,6 +126,7 @@ def test_reservation__requires_handling__pindora_api__call_fails__404(graphql): state=ReservationStateChoice.CONFIRMED, access_type=AccessType.ACCESS_CODE, access_code_is_active=True, + access_code_generated_at=local_datetime(), ) graphql.login_with_superuser() @@ -137,3 +141,25 @@ def test_reservation__requires_handling__pindora_api__call_fails__404(graphql): assert reservation.access_code_is_active is True assert PindoraClient.deactivate_reservation_access_code.call_count == 1 + + +@patch_method(PindoraClient.deactivate_reservation_access_code) +def test_reservation__requires_handling__pindora_api__not_called_if_not_generated(graphql): + reservation = ReservationFactory.create_for_requires_handling( + state=ReservationStateChoice.DENIED, + access_type=AccessType.ACCESS_CODE, + access_code_is_active=False, + access_code_generated_at=None, + ) + + graphql.login_with_superuser() + input_data = get_require_handling_data(reservation) + response = graphql(REQUIRE_HANDLING_MUTATION, input_data=input_data) + + assert response.has_errors is False, response.errors + + reservation.refresh_from_db() + assert reservation.state == ReservationStateChoice.REQUIRES_HANDLING + assert reservation.access_code_is_active is False + + assert PindoraClient.deactivate_reservation_access_code.call_count == 0 diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py index dd193d5b7d..b8a4c0d11d 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py @@ -56,14 +56,21 @@ def validate(self, data: ReservationApproveData) -> ReservationApproveData: return data def update(self, instance: Reservation, validated_data: ReservationApproveData) -> Reservation: - instance = super().update(instance=instance, validated_data=validated_data) - if self.instance.access_type == AccessType.ACCESS_CODE: # Allow activation in Pindora to fail, will be handled by a background task. with suppress(Exception): - PindoraClient.activate_reservation_access_code(reservation=instance) - instance.access_code_is_active = True - instance.save(update_fields=["access_code_is_active"]) + # If access code has not been generated (e.g. returned to handling after a deny and then approved), + # create a new active access code in Pindora. + if instance.access_code_generated_at is None: + response = PindoraClient.create_reservation(reservation=instance, is_active=True) + validated_data["access_code_generated_at"] = response["access_code_generated_at"] + validated_data["access_code_is_active"] = response["access_code_is_active"] + + else: + PindoraClient.activate_reservation_access_code(reservation=instance) + validated_data["access_code_is_active"] = True + + instance = super().update(instance=instance, validated_data=validated_data) EmailService.send_reservation_approved_email(reservation=instance) EmailService.send_staff_notification_reservation_made_email(reservation=instance) diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/requires_handling_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/requires_handling_serializers.py index 5abca1b42c..e63610c7d1 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/requires_handling_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/requires_handling_serializers.py @@ -50,7 +50,8 @@ def validate(self, data: ReservationHandlingData) -> ReservationHandlingData: return data def update(self, instance: Reservation, validated_data: dict[str, Any]) -> Reservation: - if self.instance.access_type == AccessType.ACCESS_CODE: + # Denied reservations shouldn't have an access code. It will be regenerated if the reservation is approved. + if self.instance.access_type == AccessType.ACCESS_CODE and instance.access_code_generated_at is not None: # Allow reservation modification to succeed if reservation doesn't exist in Pindora. with suppress(PindoraNotFoundError): PindoraClient.deactivate_reservation_access_code(reservation=instance) diff --git a/tilavarauspalvelu/typing.py b/tilavarauspalvelu/typing.py index bac162fbbc..5b2c0d636b 100644 --- a/tilavarauspalvelu/typing.py +++ b/tilavarauspalvelu/typing.py @@ -234,6 +234,8 @@ class ReservationApproveData(TypedDict): state: NotRequired[ReservationStateChoice] handled_at: NotRequired[datetime.datetime] + access_code_generated_at: NotRequired[datetime.datetime | None] + access_code_is_active: NotRequired[bool] class ReservationCancellationData(TypedDict):