From 922e4090e561a1ba48a25c672fb8a2d166cd2d97 Mon Sep 17 00:00:00 2001 From: aniketde Date: Sat, 7 Mar 2026 00:38:56 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Handle=20locked=20campsites=20in?= =?UTF-8?q?=20UseDirect/ReserveCalifornia=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When reservations are cancelled on ReserveCalifornia, the affected campsites enter a locked state until a scheduled unlock time (typically 8 AM the next day). Previously, these sites were reported as "Unavailable" because only the IsFree field was checked. Now, sites with a future Lock timestamp are correctly treated as "Available". Fixes #329 Fixes #298 Co-Authored-By: Claude Opus 4.6 --- camply/containers/usedirect.py | 2 +- camply/providers/usedirect/usedirect.py | 44 +++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/camply/containers/usedirect.py b/camply/containers/usedirect.py index 6b5d50a4..89aea02d 100644 --- a/camply/containers/usedirect.py +++ b/camply/containers/usedirect.py @@ -49,7 +49,7 @@ class UseDirectAvailabilitySlice(CamplyModel): IsBlocked: Optional[bool] IsWalkin: Optional[bool] ReservationId: Optional[int] - Lock: Any + Lock: Optional[datetime.datetime] MinStay: Optional[int] IsReservationDraw: Optional[bool] diff --git a/camply/providers/usedirect/usedirect.py b/camply/providers/usedirect/usedirect.py index f2b6ff2d..add113f4 100644 --- a/camply/providers/usedirect/usedirect.py +++ b/camply/providers/usedirect/usedirect.py @@ -408,6 +408,7 @@ def get_campsites( campsites: List[AvailableCampsite] = [] if availability_response.Facility.Units is None: return campsites + locked_count = 0 for _campground_unit_id, unit in availability_response.Facility.Units.items(): for _slice_date, availability_slice in unit.Slices.items(): campsite = self._get_available_campsite( @@ -422,6 +423,17 @@ def get_campsites( or campsite.campsite_id in self.campsite_ids ): campsites.append(campsite) + elif ( + availability_slice.Lock is not None + and availability_slice.Lock > datetime.now() + ): + locked_count += 1 + if locked_count > 0: + logger.info( + "%s locked site(s) found that will become available " + "for booking at their scheduled unlock time (typically 8 AM).", + locked_count, + ) return campsites def _get_available_campsite( @@ -464,9 +476,7 @@ def _get_available_campsite( booking_end_date=start_date + timedelta(days=1), booking_nights=1, campsite_site_name=unit.Name, - availability_status=( - "Available" if availability_slice.IsFree is True else "Unavailable" - ), + availability_status=self._get_availability_status(availability_slice), recreation_area=recreation_area.recreation_area, recreation_area_id=facility.recreation_area_id, facility_name=facility.facility_name, @@ -482,6 +492,34 @@ def _get_available_campsite( ) return campsite + @staticmethod + def _get_availability_status( + availability_slice: UseDirectAvailabilitySlice, + ) -> str: + """ + Determine availability status from an availability slice. + + Sites that are not free but have a future Lock timestamp are + cancelled reservations that will become bookable at the lock + time (typically 8 AM the next day). + + Parameters + ---------- + availability_slice: UseDirectAvailabilitySlice + + Returns + ------- + str + """ + if availability_slice.IsFree is True: + return "Available" + if ( + availability_slice.Lock is not None + and availability_slice.Lock > datetime.now() + ): + return "Available" + return "Unavailable" + def _fetch_metadata_from_disk( self, file_path: pathlib.Path ) -> Optional[Union[Dict[Any, Any], List[Dict[Any, Any]]]]: