Skip to content

Commit

Permalink
Use override date periods closed times in generating reservation series
Browse files Browse the repository at this point in the history
  • Loading branch information
ranta authored and matti-lamppu committed Sep 18, 2024
1 parent bae0122 commit ec0cf05
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 78 deletions.
41 changes: 23 additions & 18 deletions applications/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import datetime
from typing import TYPE_CHECKING

from django.conf import settings
from django.db import transaction
Expand All @@ -8,18 +7,17 @@
from actions.recurring_reservation import ReservationDetails
from applications.enums import ApplicantTypeChoice, Weekday
from applications.models import Address, AllocatedTimeSlot, Organisation, Person
from common.date_utils import local_end_of_day, local_start_of_day
from common.utils import translate_for_user
from opening_hours.errors import ReservableTimeSpanClientError
from opening_hours.utils.reservable_time_span_client import ReservableTimeSpanClient
from opening_hours.enums import HaukiResourceState
from opening_hours.utils.hauki_api_client import HaukiAPIClient
from opening_hours.utils.time_span_element import TimeSpanElement
from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice
from reservations.models import RecurringReservation
from reservations.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task
from tilavarauspalvelu.celery import app
from utils.sentry import SentryLogger

if TYPE_CHECKING:
from opening_hours.utils.time_span_element import TimeSpanElement


def _get_series_override_closed_time_spans(allocations: list[AllocatedTimeSlot]) -> dict[int, list["TimeSpanElement"]]:
"""
Expand All @@ -34,20 +32,27 @@ def _get_series_override_closed_time_spans(allocations: list[AllocatedTimeSlot])
if resource is None or resource.id in closed_time_spans:
continue # Skip if already fetched

try:
client = ReservableTimeSpanClient(resource)
except ReservableTimeSpanClientError:
# Skip fetching opening hours if the resource indicates that it never has any opening hours.
# There is a slight chance that someone has updated the resource recently,
# and it didn't have any opening hour rules previously,
# but since there is a performance penalty, we still skip fetching for now.
continue

application_round = allocation.reservation_unit_option.application_section.application.application_round
closed_time_spans[resource.id] = client.get_closed_time_spans(
start_date=application_round.reservation_period_begin,
end_date=application_round.reservation_period_end,

# Fetch periods from Hauki API
date_periods = HaukiAPIClient.get_date_periods(
hauki_resource_id=resource.id,
start_date_lte=application_round.reservation_period_end.isoformat(), # Starts before period ends
end_date_gte=application_round.reservation_period_begin.isoformat(), # Ends after period begins
)

# Convert periods to TimeSpanElements
closed_time_spans[resource.id] = [
TimeSpanElement(
start_datetime=local_start_of_day(datetime.date.fromisoformat(period["start_date"])),
end_datetime=local_end_of_day(datetime.date.fromisoformat(period["end_date"])),
is_reservable=False,
)
for period in date_periods
# Overriding closed date periods are exceptions to the normal opening hours
if period["override"] and period["resource_state"] == HaukiResourceState.CLOSED.value
]

return closed_time_spans


Expand Down
8 changes: 0 additions & 8 deletions opening_hours/utils/reservable_time_span_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,6 @@ def run(self) -> list[ReservableTimeSpan]:

return created_reservable_time_spans

def get_closed_time_spans(self, start_date: datetime.date, end_date: datetime.date) -> list[TimeSpanElement]:
"""Get closed time spans for the resource."""
self.start_date = start_date
self.end_date = end_date
opening_hours_response = self._get_opening_hours_from_hauki_api()
parsed_time_spans = self._parse_opening_hours(opening_hours_response)
return self._merge_overlapping_closed_time_spans(parsed_time_spans)

def _init_date_range(self) -> None:
today = local_date()
if self.origin_hauki_resource.latest_fetched_date:
Expand Down
131 changes: 79 additions & 52 deletions tests/test_utils/test_generate_reservations_from_allocations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@
from common.date_utils import DEFAULT_TIMEZONE, combine, local_date, local_datetime, local_iso_format
from opening_hours.enums import HaukiResourceState
from opening_hours.utils.hauki_api_client import HaukiAPIClient
from opening_hours.utils.hauki_api_types import (
HaukiAPIOpeningHoursResponseDate,
HaukiAPIOpeningHoursResponseItem,
HaukiAPIOpeningHoursResponseResource,
HaukiAPIOpeningHoursResponseTime,
HaukiTranslatedField,
)
from opening_hours.utils.hauki_api_types import HaukiAPIDatePeriod
from reservation_units.models import ReservationUnitHierarchy
from reservations.enums import (
CustomerTypeChoice,
Expand All @@ -32,27 +26,16 @@
pytest.mark.django_db,
]

NEXT_YEAR = local_date().year + 1
EMPTY_RESPONSE = HaukiAPIOpeningHoursResponseItem(
resource=HaukiAPIOpeningHoursResponseResource(
id=1,
name=HaukiTranslatedField(fi="Test resource", sv=None, en=None),
timezone="Europe/Helsinki",
origins=[],
),
opening_hours=[],
)


@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)
@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation(num=3)

application_round = slot.reservation_unit_option.application_section.application.application_round
generate_reservation_series_from_allocations(application_round_id=application_round.id)

assert HaukiAPIClient.get_resource_opening_hours.call_count == 1
assert HaukiAPIClient.get_date_periods.call_count == 1

series: list[RecurringReservation] = list(RecurringReservation.objects.all())
assert len(series) == 1
Expand Down Expand Up @@ -107,7 +90,7 @@ def test_generate_reservation_series_from_allocations():
assert RejectedOccurrence.objects.count() == 0


@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)
@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__individual():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation(applicant_type=ApplicantTypeChoice.INDIVIDUAL)
Expand All @@ -117,7 +100,6 @@ def test_generate_reservation_series_from_allocations__individual():

series: list[RecurringReservation] = list(RecurringReservation.objects.all())
assert len(series) == 1

assert series[0].allocated_time_slot == slot

reservations: list[Reservation] = list(series[0].reservations.order_by("begin").all())
Expand All @@ -130,9 +112,9 @@ def test_generate_reservation_series_from_allocations__individual():
assert reservations[0].reservee_address_city == application.billing_address.city
assert reservations[0].reservee_address_zip == application.billing_address.post_code

assert HaukiAPIClient.get_date_periods.call_count == 1


@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
@pytest.mark.parametrize(
"applicant_type",
[
Expand All @@ -141,6 +123,8 @@ def test_generate_reservation_series_from_allocations__individual():
ApplicantTypeChoice.COMMUNITY,
],
)
@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__non_individual(applicant_type):
slot = AllocatedTimeSlotFactory.create_ready_for_reservation(applicant_type=applicant_type)

Expand All @@ -167,8 +151,10 @@ def test_generate_reservation_series_from_allocations__non_individual(applicant_
assert reservations[0].reservee_address_city == application.organisation.address.city
assert reservations[0].reservee_address_zip == application.organisation.address.post_code

assert HaukiAPIClient.get_date_periods.call_count == 1

@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)

@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__multiple_allocations():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation()
Expand All @@ -193,8 +179,10 @@ def test_generate_reservation_series_from_allocations__multiple_allocations():
reservations: list[Reservation] = list(series[1].reservations.all())
assert len(reservations) == 1

assert HaukiAPIClient.get_date_periods.call_count == 1


@patch_method(HaukiAPIClient.get_resource_opening_hours, side_effect=ValueError("Test error"))
@patch_method(HaukiAPIClient.get_date_periods, side_effect=ValueError("Test error"))
@patch_method(SentryLogger.log_exception)
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__error_handling():
Expand All @@ -207,8 +195,10 @@ def test_generate_reservation_series_from_allocations__error_handling():
# Errors are logged to Sentry.
assert SentryLogger.log_exception.call_count == 1

assert HaukiAPIClient.get_date_periods.call_count == 1

@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)

@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__invalid_start_interval():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation(
Expand All @@ -231,8 +221,13 @@ def test_generate_reservation_series_from_allocations__invalid_start_interval():
assert local_iso_format(rejected[0].begin_datetime) == local_datetime(2024, 1, 1, hour=12, minute=1).isoformat()
assert local_iso_format(rejected[0].end_datetime) == local_datetime(2024, 1, 1, hour=14).isoformat()

assert HaukiAPIClient.get_date_periods.call_count == 1


NEXT_YEAR = local_date().year + 1


@patch_method(HaukiAPIClient.get_resource_opening_hours, return_value=EMPTY_RESPONSE)
@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(NEXT_YEAR, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__overlapping_reservation():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation(num=2)
Expand Down Expand Up @@ -264,37 +259,23 @@ def test_generate_reservation_series_from_allocations__overlapping_reservation()
assert local_iso_format(rejected[0].begin_datetime) == local_datetime(NEXT_YEAR, 1, 1, hour=12).isoformat()
assert local_iso_format(rejected[0].end_datetime) == local_datetime(NEXT_YEAR, 1, 1, hour=14).isoformat()

assert HaukiAPIClient.get_date_periods.call_count == 1

@patch_method(HaukiAPIClient.get_resource_opening_hours)

@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__explicitly_closed_opening_hours():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation()
application_round = slot.reservation_unit_option.application_section.application.application_round

HaukiAPIClient.get_resource_opening_hours.return_value = HaukiAPIOpeningHoursResponseItem(
resource=HaukiAPIOpeningHoursResponseResource(
id=1,
name=HaukiTranslatedField(fi="Test resource", sv=None, en=None),
timezone="Europe/Helsinki",
HaukiAPIClient.get_date_periods.return_value = [
HaukiAPIDatePeriod(
start_date="2024-01-01",
end_date="2024-01-01",
resource_state=HaukiResourceState.CLOSED.value,
override=True,
),
opening_hours=[
HaukiAPIOpeningHoursResponseDate(
date=application_round.reservation_period_begin.isoformat(),
times=[
HaukiAPIOpeningHoursResponseTime(
name="Test time",
description="Test description",
start_time=slot.begin_time.isoformat(),
end_time=slot.end_time.isoformat(),
end_time_on_next_day=False,
resource_state=HaukiResourceState.CLOSED.value,
full_day=False,
periods=[1],
)
],
)
],
)
]

generate_reservation_series_from_allocations(application_round_id=application_round.id)

Expand All @@ -310,3 +291,49 @@ def test_generate_reservation_series_from_allocations__explicitly_closed_opening
assert rejected[0].rejection_reason == RejectionReadinessChoice.RESERVATION_UNIT_CLOSED
assert local_iso_format(rejected[0].begin_datetime) == local_datetime(2024, 1, 1, hour=12).isoformat()
assert local_iso_format(rejected[0].end_datetime) == local_datetime(2024, 1, 1, hour=14).isoformat()

assert HaukiAPIClient.get_date_periods.call_count == 1


@patch_method(HaukiAPIClient.get_date_periods, return_value=[])
@freezegun.freeze_time(datetime.datetime(2024, 1, 1, tzinfo=DEFAULT_TIMEZONE))
def test_generate_reservation_series_from_allocations__explicitly_closed_opening_hours__not_affecting():
slot = AllocatedTimeSlotFactory.create_ready_for_reservation()
application_round = slot.reservation_unit_option.application_section.application.application_round

HaukiAPIClient.get_date_periods.return_value = [
# Wrong date
HaukiAPIDatePeriod(
start_date="2024-01-02",
end_date="2024-01-02",
resource_state=HaukiResourceState.CLOSED.value,
override=True,
),
# Non-overriding date period
HaukiAPIDatePeriod(
start_date="2024-01-01",
end_date="2024-01-01",
resource_state=HaukiResourceState.CLOSED.value,
override=False,
),
# Not closed
HaukiAPIDatePeriod(
start_date="2024-01-01",
end_date="2024-01-01",
resource_state=HaukiResourceState.WITH_RESERVATION.value,
override=True,
),
]

generate_reservation_series_from_allocations(application_round_id=application_round.id)

series: list[RecurringReservation] = list(RecurringReservation.objects.all())
assert len(series) == 1

reservations: list[Reservation] = list(series[0].reservations.all())
assert len(reservations) == 1

rejected = list(RejectedOccurrence.objects.all())
assert len(rejected) == 0

assert HaukiAPIClient.get_date_periods.call_count == 1

0 comments on commit ec0cf05

Please sign in to comment.