Skip to content

Commit

Permalink
Create an access code when approving a reservation without one
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matti-lamppu committed Jan 30, 2025
1 parent 040f3ca commit 6cf0101
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 16 deletions.
55 changes: 45 additions & 10 deletions tests/test_graphql_api/test_reservation/test_approve.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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
26 changes: 26 additions & 0 deletions tests/test_graphql_api/test_reservation/test_requires_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions tilavarauspalvelu/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 6cf0101

Please sign in to comment.