From b8f7307ebe724dc0ee905f8818baf1bd5e2b60b6 Mon Sep 17 00:00:00 2001 From: Chenliang Xu Date: Sun, 31 May 2026 16:52:46 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20filter=20walk-in=20/=20non-web-b?= =?UTF-8?q?ookable=20campsites=20by=20default=20(UseDirect)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The bug The UseDirect API (ReserveCalifornia and siblings) marks walk-in / non-web-bookable nights as `availability_status="Available"`, the same status reservable sites get. Camply forwards every "Available" night to the user, so cancellation watchers fire false-positive notifications on sites that physically cannot be reserved online — the user races to ReserveCalifornia.com, picks the date, and the site isn't even listed. Concrete reproducer at Sugarloaf Ridge SP (campground id 731) for 2026-06-06: camply campsites --provider ReserveCalifornia --campground 731 \ --start-date 2026-06-06 --end-date 2026-06-07 --nights 1 Reports 3 "available" sites: #04, #05, #35. None of the three can actually be booked via the website. Raw API dump for those same units: site AllowWebBooking IsWalkin #04 True True #05 False True #35 False True The data is already there in `UseDirectAvailabilityUnit.AllowWebBooking` (per-site) and `UseDirectAvailabilitySlice.IsWalkin` (per-night). Camply just wasn't reading them when deciding whether to surface a site. Note that `AllowWebBooking=True` does not imply bookable — site #04 is normally bookable but held back for walk-ups on this specific date, which is what `IsWalkin` captures. Both flags must be checked. ## The fix Treat a slice as bookable only when: availability_slice.IsWalkin is not True and unit.AllowWebBooking is not False The check is in the same loop in `UseDirectProvider.get_campsites` that already filters by `availability_status == "Available"` — minimal surface area, no new API calls. ## Escape hatch Add `--include-walkin` (off by default) so anyone who wants the old behavior — or who's specifically hunting walk-ins — can opt back in: camply campsites ... --include-walkin Plumbing follows the existing `--equipment` pattern: click option in `cli.py`, threaded through `_get_provider_kwargs_from_cli` into `provider_kwargs`, accepted by `SearchUseDirect.__init__`, forwarded to `UseDirectProvider.get_campsites`. ## Verification Same query as the reproducer above: (default) 0 Reservable Campsites Matching ✓ --include-walkin 3 Reservable Campsites Matching ✓ Other providers (Recreation.gov, GoingToCamp, Yellowstone, …) are unaffected — the param is UseDirect-specific and only the UseDirect search class reads it. Co-Authored-By: Claude Opus 4.7 --- camply/cli.py | 15 +++++++++++++++ camply/providers/usedirect/usedirect.py | 13 ++++++++++++- camply/search/search_usedirect.py | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/camply/cli.py b/camply/cli.py index 0b2330c1..110956a3 100644 --- a/camply/cli.py +++ b/camply/cli.py @@ -457,6 +457,16 @@ def campgrounds( "equipment names include `Tent`, `RV`. `Trailer`, `Vehicle` and are " "not case-sensitive.", ) +include_walkin_argument = click.option( + "--include-walkin", + is_flag=True, + show_default=True, + default=False, + help="Include walk-in / non-web-bookable campsites in results. By default " + "these are filtered out because they show as 'Available' in the API but " + "can't actually be reserved online. Currently honored by the " + "ReserveCalifornia and other UseDirect-based providers.", +) equipment_id_argument = click.option( "--equipment-id", default=None, @@ -619,6 +629,7 @@ def _get_provider_kwargs_from_cli( offline_search_path: Optional[str], equipment: Tuple[Union[str, int]], equipment_id: Tuple[Union[str, int]], + include_walkin: bool, day: Optional[Tuple[str]], ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ @@ -666,6 +677,7 @@ def _get_provider_kwargs_from_cli( "offline_search_path": offline_search_path, "equipment": equipment, "equipment_id": equipment_id, + "include_walkin": include_walkin, "days_of_the_week": days_of_the_week, } search_kwargs = { @@ -701,6 +713,7 @@ def _get_provider_kwargs_from_cli( @notify_first_try_argument @equipment_argument @equipment_id_argument +@include_walkin_argument @provider_argument @debug_option @click.pass_obj @@ -726,6 +739,7 @@ def campsites( offline_search_path: Optional[str], equipment: Tuple[Union[str, int]], equipment_id: Tuple[Union[str, int]], + include_walkin: bool, day: Optional[Tuple[str]], ) -> None: """ @@ -767,6 +781,7 @@ def campsites( offline_search_path=offline_search_path, equipment=equipment, equipment_id=equipment_id, + include_walkin=include_walkin, day=day, yaml_config=yaml_config, ) diff --git a/camply/providers/usedirect/usedirect.py b/camply/providers/usedirect/usedirect.py index 65fec63d..af4f33bb 100644 --- a/camply/providers/usedirect/usedirect.py +++ b/camply/providers/usedirect/usedirect.py @@ -367,6 +367,7 @@ def get_campsites( sleeping_unit_id: Optional[int] = None, unit_sort: Optional[str] = "orderby", in_season_only: Optional[bool] = True, + include_walkin: bool = False, ) -> List[AvailableCampsite]: """ Get Campsites from UseDirect @@ -425,7 +426,17 @@ def get_campsites( unit=unit, ) campsite_available = campsite.availability_status == "Available" - if campsite_available is True: + # The API reports walk-in / non-web-bookable slices as + # "Available" but they can't actually be reserved online. + # IsWalkin can be set per-night even on units whose + # AllowWebBooking is True (a normally bookable site that's + # held back for walk-ups on specific dates), so both flags + # matter. Pass through unfiltered when --include-walkin is set. + is_bookable = ( + availability_slice.IsWalkin is not True + and unit.AllowWebBooking is not False + ) + if campsite_available is True and (include_walkin or is_bookable): if ( len(self.campsite_ids) == 0 or campsite.campsite_id in self.campsite_ids diff --git a/camply/search/search_usedirect.py b/camply/search/search_usedirect.py index 9827ebaf..01be7946 100644 --- a/camply/search/search_usedirect.py +++ b/camply/search/search_usedirect.py @@ -53,6 +53,7 @@ def __init__( weekends_only: bool = False, campgrounds: Optional[Union[List[str], str]] = None, nights: int = 1, + include_walkin: bool = False, **kwargs, ) -> None: """ @@ -78,6 +79,7 @@ def __init__( nights=nights, **kwargs, ) + self.include_walkin: bool = include_walkin self._recreation_area_ids: List[int] = make_list(recreation_area, coerce=int) self._campground_ids: List[int] = make_list(campgrounds, coerce=int) campsites = make_list(kwargs.get("campsites", []), coerce=int) or [] @@ -145,6 +147,7 @@ def get_all_campsites(self, **kwargs: Dict[str, Any]) -> List[AvailableCampsite] campground_id=campground.facility_id, start_date=month, end_date=end_date, + include_walkin=self.include_walkin, ) logger.info( f"\t{logging_utils.get_emoji(campsites)}\t"