Skip to content

Commit

Permalink
Update access code in Pindora on reservation reschedule
Browse files Browse the repository at this point in the history
  • Loading branch information
matti-lamppu committed Jan 28, 2025
1 parent 3e21536 commit bb91fca
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 4 deletions.
107 changes: 106 additions & 1 deletion tests/test_graphql_api/test_reservation/test_adjust_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import pytest
from django.test import override_settings

from tilavarauspalvelu.enums import ReservationStartInterval, ReservationStateChoice
from tilavarauspalvelu.enums import AccessType, ReservationStartInterval, ReservationStateChoice
from tilavarauspalvelu.integrations.email.main import EmailService
from tilavarauspalvelu.integrations.keyless_entry import PindoraClient
from tilavarauspalvelu.models import Reservation, ReservationUnitHierarchy
from utils.date_utils import DEFAULT_TIMEZONE, local_date, local_datetime

Expand Down Expand Up @@ -501,3 +502,107 @@ def test_reservation__adjust_time__update_reservation_buffer_on_adjust(graphql):
# New reservation unit buffers are applied automatically on adjust.
assert reservation.buffer_time_before == datetime.timedelta(hours=2)
assert reservation.buffer_time_after == datetime.timedelta(hours=2)


@patch_method(PindoraClient.reschedule_reservation)
@patch_method(PindoraClient.deactivate_reservation_access_code)
def test_reservation__adjust_time__same_access_type(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.ACCESS_CODE,
)

graphql.login_with_superuser()
data = get_adjust_data(reservation)
response = graphql(ADJUST_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.reschedule_reservation.called is True
assert PindoraClient.deactivate_reservation_access_code.called is False


@patch_method(PindoraClient.reschedule_reservation)
@patch_method(PindoraClient.deactivate_reservation_access_code)
def test_reservation__adjust_time__same_access_type__requires_handling(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.ACCESS_CODE,
reservation_units__require_reservation_handling=True,
)

graphql.login_with_superuser()
data = get_adjust_data(reservation)
response = graphql(ADJUST_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.reschedule_reservation.called is True
assert PindoraClient.deactivate_reservation_access_code.called is True


@patch_method(
PindoraClient.create_reservation,
return_value={"access_code_generated_at": datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)},
)
def test_reservation__adjust_time__change_to_access_code(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
access_type=AccessType.UNRESTRICTED,
reservation_units__access_type=AccessType.ACCESS_CODE,
)

graphql.login_with_superuser()
data = get_adjust_data(reservation)
response = graphql(ADJUST_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.create_reservation.called is True
assert PindoraClient.create_reservation.call_args.kwargs["is_active"] is True

reservation.refresh_from_db()
assert reservation.access_code_generated_at == datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)


@patch_method(
PindoraClient.create_reservation,
return_value={"access_code_generated_at": datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)},
)
def test_reservation__adjust_time__change_to_access_code__requires_handling(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
access_type=AccessType.UNRESTRICTED,
reservation_units__access_type=AccessType.ACCESS_CODE,
reservation_units__require_reservation_handling=True,
)

graphql.login_with_superuser()
data = get_adjust_data(reservation)
response = graphql(ADJUST_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.create_reservation.called is True
assert PindoraClient.create_reservation.call_args.kwargs["is_active"] is False

reservation.refresh_from_db()
assert reservation.access_code_generated_at == datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)


@patch_method(PindoraClient.delete_reservation)
def test_reservation__adjust_time__change_from_access_code(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.UNRESTRICTED,
access_code_generated_at=datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE),
)

graphql.login_with_superuser()
data = get_adjust_data(reservation)
response = graphql(ADJUST_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.delete_reservation.called is True

reservation.refresh_from_db()
assert reservation.access_code_generated_at is None
113 changes: 112 additions & 1 deletion tests/test_graphql_api/test_reservation/test_staff_adjust_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import pytest
from django.test import override_settings

from tilavarauspalvelu.enums import ReservationStartInterval, ReservationStateChoice, ReservationTypeChoice
from tilavarauspalvelu.enums import AccessType, ReservationStartInterval, ReservationStateChoice, ReservationTypeChoice
from tilavarauspalvelu.integrations.keyless_entry import PindoraClient
from tilavarauspalvelu.models import Reservation, ReservationUnitHierarchy
from utils.date_utils import DEFAULT_TIMEZONE, local_datetime, next_hour

Expand All @@ -18,6 +19,7 @@
SpaceFactory,
UserFactory,
)
from tests.helpers import patch_method

from .helpers import ADJUST_STAFF_MUTATION, get_staff_adjust_data

Expand Down Expand Up @@ -531,3 +533,112 @@ def test_reservation__staff_adjust_time__reservation_block_whole_day__ignore_giv
assert reservation.end == datetime.datetime(2023, 1, 1, 13, tzinfo=DEFAULT_TIMEZONE)
assert reservation.buffer_time_before == datetime.timedelta(hours=12)
assert reservation.buffer_time_after == datetime.timedelta(hours=11)


@patch_method(PindoraClient.reschedule_reservation)
@patch_method(PindoraClient.deactivate_reservation_access_code)
def test_reservation__staff_adjust_time__same_access_type(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
type=ReservationTypeChoice.STAFF,
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.ACCESS_CODE,
)

graphql.login_with_superuser()
data = get_staff_adjust_data(reservation)
response = graphql(ADJUST_STAFF_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.reschedule_reservation.called is True
assert PindoraClient.deactivate_reservation_access_code.called is False


@patch_method(PindoraClient.reschedule_reservation)
@patch_method(PindoraClient.deactivate_reservation_access_code)
def test_reservation__staff_adjust_time__same_access_type__requires_handling(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
type=ReservationTypeChoice.STAFF,
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.ACCESS_CODE,
reservation_units__require_reservation_handling=True,
)

graphql.login_with_superuser()
data = get_staff_adjust_data(reservation)
response = graphql(ADJUST_STAFF_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.reschedule_reservation.called is True
assert PindoraClient.deactivate_reservation_access_code.called is False


@patch_method(
PindoraClient.create_reservation,
return_value={"access_code_generated_at": datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)},
)
def test_reservation__staff_adjust_time__change_to_access_code(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
type=ReservationTypeChoice.STAFF,
access_type=AccessType.UNRESTRICTED,
reservation_units__access_type=AccessType.ACCESS_CODE,
)

graphql.login_with_superuser()
data = get_staff_adjust_data(reservation)
response = graphql(ADJUST_STAFF_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.create_reservation.called is True
assert PindoraClient.create_reservation.call_args.kwargs["is_active"] is True

reservation.refresh_from_db()
assert reservation.access_code_generated_at == datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)


@patch_method(
PindoraClient.create_reservation,
return_value={"access_code_generated_at": datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)},
)
def test_reservation__staff_adjust_time__change_to_access_code__requires_handling(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
type=ReservationTypeChoice.STAFF,
access_type=AccessType.UNRESTRICTED,
reservation_units__access_type=AccessType.ACCESS_CODE,
reservation_units__require_reservation_handling=True,
)

graphql.login_with_superuser()
data = get_staff_adjust_data(reservation)
response = graphql(ADJUST_STAFF_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.create_reservation.called is True
assert PindoraClient.create_reservation.call_args.kwargs["is_active"] is True

reservation.refresh_from_db()
assert reservation.access_code_generated_at == datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE)


@patch_method(PindoraClient.delete_reservation)
def test_reservation__staff_adjust_time__change_from_access_code(graphql):
reservation = ReservationFactory.create_for_time_adjustment(
type=ReservationTypeChoice.STAFF,
access_type=AccessType.ACCESS_CODE,
reservation_units__access_type=AccessType.UNRESTRICTED,
access_code_generated_at=datetime.datetime(2025, 1, 1, tzinfo=DEFAULT_TIMEZONE),
)

graphql.login_with_superuser()
data = get_staff_adjust_data(reservation)
response = graphql(ADJUST_STAFF_MUTATION, input_data=data)

assert response.has_errors is False, response.errors

assert PindoraClient.delete_reservation.called is True

reservation.refresh_from_db()
assert reservation.access_code_generated_at is None
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING

from django.db import transaction
from graphene_django_extensions import NestingModelSerializer
from graphene_django_extensions.fields import EnumFriendlyChoiceField
from rest_framework.fields import IntegerField
Expand Down Expand Up @@ -77,11 +78,17 @@ def validate(self, data: ReservationAdjustTimeData) -> ReservationAdjustTimeData

data["buffer_time_before"] = reservation_unit.actions.get_actual_before_buffer(begin)
data["buffer_time_after"] = reservation_unit.actions.get_actual_after_buffer(end)
data["access_type"] = reservation_unit.actions.get_access_type_at(begin)

return data

def update(self, instance: Reservation, validated_data: ReservationAdjustTimeData) -> Reservation:
instance = super().update(instance=instance, validated_data=validated_data)
access_type_before = instance.access_type

with transaction.atomic():
instance = super().update(instance=instance, validated_data=validated_data)

instance.actions.create_or_update_access_code_if_required(from_access_type=access_type_before)

EmailService.send_reservation_modified_email(reservation=instance)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING, Any

from django.db import transaction
from graphene_django_extensions import NestingModelSerializer
from graphene_django_extensions.fields import EnumFriendlyChoiceField
from rest_framework.fields import IntegerField
Expand Down Expand Up @@ -61,10 +62,17 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:
ignore_ids=[self.instance.pk],
)

data["access_type"] = reservation_unit.actions.get_access_type_at(begin)

return data

def update(self, instance: Reservation, validated_data: dict[str, Any]) -> Reservation:
instance = super().update(instance=instance, validated_data=validated_data)
access_type_before = instance.access_type

with transaction.atomic():
instance = super().update(instance=instance, validated_data=validated_data)

instance.actions.create_or_update_access_code_if_required(from_access_type=access_type_before)

EmailService.send_reservation_modified_email(reservation=instance)
EmailService.send_staff_notification_reservation_requires_handling_email(reservation=instance)
Expand Down
35 changes: 35 additions & 0 deletions tilavarauspalvelu/models/reservation/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datetime
import uuid
from contextlib import suppress
from typing import TYPE_CHECKING, Literal

from django.conf import settings
Expand All @@ -10,16 +11,20 @@
from icalendar import Calendar, Event, Timezone, TimezoneDaylight, TimezoneStandard

from tilavarauspalvelu.enums import (
AccessType,
CalendarProperty,
CustomerTypeChoice,
EventProperty,
OrderStatus,
PaymentType,
ReservationStateChoice,
ReservationTypeChoice,
TimezoneProperty,
TimezoneRuleProperty,
)
from tilavarauspalvelu.exceptions import ReservationPriceCalculationError
from tilavarauspalvelu.integrations.keyless_entry import PindoraClient
from tilavarauspalvelu.integrations.keyless_entry.exceptions import PindoraNotFoundError
from tilavarauspalvelu.integrations.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient
from tilavarauspalvelu.models import ApplicationSection, ReservationMetadataField, Space
from tilavarauspalvelu.translation import get_attr_by_language, get_translated
Expand Down Expand Up @@ -293,3 +298,33 @@ def refund_paid_reservation(self) -> None:

payment_order.status = OrderStatus.REFUNDED
payment_order.save(update_fields=["refund_id", "status"])

def create_or_update_access_code_if_required(self, *, from_access_type: AccessType) -> None:
"""Notify Pindora about the time of the reservation if required."""
current_access_type = self.reservation.access_type

is_active = (
self.reservation.state == ReservationStateChoice.CONFIRMED
and self.reservation.type != ReservationTypeChoice.BLOCKED
)

# Access type remains 'ACCESS_CODE', reschedule the reservation
if {from_access_type, current_access_type} == {AccessType.ACCESS_CODE}:
with suppress(PindoraNotFoundError):
if not is_active:
PindoraClient.deactivate_reservation_access_code(reservation=self.reservation)
PindoraClient.reschedule_reservation(reservation=self.reservation)

# Access type no longer 'ACCESS_CODE', delete the reservation
elif from_access_type == AccessType.ACCESS_CODE:
with suppress(PindoraNotFoundError):
PindoraClient.delete_reservation(reservation=self.reservation)
self.reservation.access_code_generated_at = None
self.reservation.save(update_fields=["access_code_generated_at"])

# Access type now 'ACCESS_CODE', create access code
elif current_access_type == AccessType.ACCESS_CODE:
with suppress(PindoraNotFoundError):
response = PindoraClient.create_reservation(reservation=self.reservation, is_active=is_active)
self.reservation.access_code_generated_at = response["access_code_generated_at"]
self.reservation.save(update_fields=["access_code_generated_at"])
1 change: 1 addition & 0 deletions tilavarauspalvelu/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class ReservationAdjustTimeData(TypedDict):
state: NotRequired[ReservationStateChoice]
buffer_time_before: NotRequired[datetime.timedelta]
buffer_time_after: NotRequired[datetime.timedelta]
access_type: NotRequired[AccessType]


class ReservationApproveData(TypedDict):
Expand Down

0 comments on commit bb91fca

Please sign in to comment.