diff --git a/backend/src/appointment/controller/apis/google_client.py b/backend/src/appointment/controller/apis/google_client.py index 1b0371c97..bbb5aa23a 100644 --- a/backend/src/appointment/controller/apis/google_client.py +++ b/backend/src/appointment/controller/apis/google_client.py @@ -140,7 +140,7 @@ def create_event(self, calendar_id, body, token): response = None with build("calendar", "v3", credentials=token, cache_discovery=False) as service: try: - response = service.events().insert(calendarId=calendar_id, sendUpdates="all", body=body).execute() + response = service.events().insert(calendarId=calendar_id, body=body).execute() except HttpError as e: logging.warning(f"[google_client.create_event] Request Error: {e.status_code}/{e.error_details}") diff --git a/backend/src/appointment/controller/calendar.py b/backend/src/appointment/controller/calendar.py index 470d44e23..47aa46ba4 100644 --- a/backend/src/appointment/controller/calendar.py +++ b/backend/src/appointment/controller/calendar.py @@ -303,7 +303,7 @@ def available_slots_from_schedule(s: models.Schedule) -> list[schemas.SlotBase]: # We add a day here because it should be inclusive of the final day. farthest_booking = now + timedelta(days=1, minutes=s.farthest_booking) - schedule_start = datetime.combine(s.start_date, s.start_time) + schedule_start = max([datetime.combine(s.start_date, s.start_time), earliest_booking]) schedule_end = min([datetime.combine(s.end_date, s.end_time), farthest_booking]) if s.end_date else farthest_booking start_time = datetime.combine(now.min, s.start_time) - datetime.min @@ -319,8 +319,9 @@ def available_slots_from_schedule(s: models.Schedule) -> list[schemas.SlotBase]: weekdays = [1, 2, 3, 4, 5] # Between the available booking time - for day in range(earliest_booking.day, schedule_end.day): - current_datetime = datetime(year=schedule_start.year, month=schedule_start.month, day=day) + for ordinal in range(schedule_start.toordinal(), schedule_end.toordinal()): + date = datetime.fromordinal(ordinal) + current_datetime = datetime(year=date.year, month=date.month, day=date.day) # Check if this weekday is within our schedule if current_datetime.isoweekday() in weekdays: # Generate each timeslot based on the selected duration diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py index 1ea032204..1e6f8e51f 100644 --- a/backend/src/appointment/routes/schedule.py +++ b/backend/src/appointment/routes/schedule.py @@ -101,26 +101,26 @@ def read_schedule_availabilities( if not schedule.active: raise validation.ScheduleNotFoundException() - # calculate theoretically possible slots from schedule config - availableSlots = Tools.available_slots_from_schedule(schedule) - - # get all events from all connected calendars in scheduled date range calendars = repo.get_calendars_by_subscriber(db, subscriber.id, False) if not calendars or len(calendars) == 0: raise validation.CalendarNotFoundException() - existingEvents = Tools.existing_events_for_schedule(schedule, calendars, subscriber, google_client, db) - actualSlots = Tools.events_set_difference(availableSlots, existingEvents) + # calculate theoretically possible slots from schedule config + available_slots = Tools.available_slots_from_schedule(schedule) + + # get all events from all connected calendars in scheduled date range + existing_slots = Tools.existing_events_for_schedule(schedule, calendars, subscriber, google_client, db) + actual_slots = Tools.events_set_difference(available_slots, existing_slots) - if not actualSlots or len(actualSlots) == 0: + if not actual_slots or len(actual_slots) == 0: raise validation.SlotNotFoundException() return schemas.AppointmentOut( title=schedule.name, details=schedule.details, owner_name=subscriber.name, - slots=actualSlots, + slots=actual_slots, ) diff --git a/backend/test/factory/subscriber_factory.py b/backend/test/factory/subscriber_factory.py index 94ff925e5..e6351e7b1 100644 --- a/backend/test/factory/subscriber_factory.py +++ b/backend/test/factory/subscriber_factory.py @@ -13,7 +13,7 @@ def _make_subscriber(level, name=FAKER_RANDOM_VALUE, username=FAKER_RANDOM_VALUE with with_db() as db: subscriber = repo.create_subscriber(db, schemas.SubscriberBase( name=name if factory_has_value(name) else fake.name(), - username=username if factory_has_value(username) else fake.name(), + username=username if factory_has_value(username) else fake.name().replace(' ', '_'), email=email if factory_has_value(email) else fake.email(), level=level, timezone='America/Vancouver' diff --git a/backend/test/integration/test_schedule.py b/backend/test/integration/test_schedule.py index 07215fca5..1f33aea1a 100644 --- a/backend/test/integration/test_schedule.py +++ b/backend/test/integration/test_schedule.py @@ -1,4 +1,10 @@ import os +from datetime import date, time + +from freezegun import freeze_time + +from appointment.controller.auth import signed_url_by_subscriber +from appointment.controller.calendar import CalDavConnector from defines import DAY1, DAY5, DAY14, auth_headers, DAY2 @@ -210,3 +216,80 @@ def test_update_foreign_schedule(self, with_client, make_pro_subscriber, make_ca headers=auth_headers, ) assert response.status_code == 403, response.text + + def test_public_availability(self, monkeypatch, with_client, make_pro_subscriber, make_caldav_calendar, make_schedule): + class MockCaldavConnector: + @staticmethod + def __init__(self, url, user, password): + """We don't want to initialize a client""" + pass + + @staticmethod + def list_events(self, start, end): + return [] + + monkeypatch.setattr(CalDavConnector, "__init__", MockCaldavConnector.__init__) + monkeypatch.setattr(CalDavConnector, "list_events", MockCaldavConnector.list_events) + + start_date = date(2024, 4, 1) + start_time = time(9) + end_time = time(17) + + subscriber = make_pro_subscriber() + generated_calendar = make_caldav_calendar(subscriber.id, connected=True) + make_schedule( + calendar_id=generated_calendar.id, + active=True, + start_date=start_date, + start_time=start_time, + end_time=end_time, + end_date=None, + earliest_booking=1440, + farthest_booking=20160, + slot_duration=30) + + signed_url = signed_url_by_subscriber(subscriber) + + # Check availability at the start of the schedule + with freeze_time(start_date): + response = with_client.post( + "/schedule/public/availability", + json={"url": signed_url}, + headers=auth_headers, + ) + assert response.status_code == 200, response.text + data = response.json() + slots = data['slots'] + + # Based off the earliest_booking our earliest slot is tomorrow at 9:00am + assert slots[0]['start'] == '2024-04-02T09:00:00' + # Based off the farthest_booking our latest slot is 4:30pm + assert slots[-1]['start'] == '2024-04-15T16:30:00' + + # Check availability over a year from now + with freeze_time(date(2025, 6, 1)): + response = with_client.post( + "/schedule/public/availability", + json={"url": signed_url}, + headers=auth_headers, + ) + assert response.status_code == 200, response.text + data = response.json() + slots = data['slots'] + + assert slots[0]['start'] == '2025-06-02T09:00:00' + assert slots[-1]['start'] == '2025-06-13T16:30:00' + + # Check availability with a start date day greater than the farthest_booking day + with freeze_time(date(2025, 6, 27)): + response = with_client.post( + "/schedule/public/availability", + json={"url": signed_url}, + headers=auth_headers, + ) + assert response.status_code == 200, response.text + data = response.json() + slots = data['slots'] + + assert slots[0]['start'] == '2025-06-30T09:00:00' + assert slots[-1]['start'] == '2025-07-11T16:30:00'