From 78a0e026ba6c0526d4d218161880e8a616143915 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:49:39 +0530 Subject: [PATCH 1/9] fix holds for multi-resource bookings --- src/slotify/availability.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/slotify/availability.py b/src/slotify/availability.py index 2ff5a5c..9f608fa 100644 --- a/src/slotify/availability.py +++ b/src/slotify/availability.py @@ -359,6 +359,7 @@ def _candidate( slot: Slot, *, resource_id: str | None = None, + resource_ids: tuple[str, ...] | None = None, metadata=None, series_id: str | None = None, ) -> Booking: @@ -369,6 +370,13 @@ def _candidate( metadata=metadata, series_id=series_id, ) + if resource_ids is not None: + return self._booking_for( + slot, + resource_ids=resource_ids, + metadata=metadata, + series_id=series_id, + ) return self._booking_for( slot, metadata=metadata, @@ -685,7 +693,10 @@ def hold( if self.hold_store.conflicts(candidate, now=current): raise BookingConflictError("The requested slot is already held.") return self.hold_store.create(BookingHold( - slot=slot, expires_at=expires_at, metadata=metadata or {} + slot=slot, + expires_at=expires_at, + resource_ids=candidate.effective_resource_ids, + metadata=metadata or {}, )) def confirm_hold( @@ -703,6 +714,11 @@ def confirm_hold( booking = self._candidate( hold.slot, resource_id=hold.resource_id, + resource_ids=( + hold.resource_ids + if hold.resource_id is None and hold.resource_ids + else None + ), metadata=metadata if metadata is not None else hold.metadata, ) if self.hold_store.conflicts(booking, now=current, exclude_hold_id=hold_id): From 0c979cd9a5a3a350d4c1ab167f6c101b2d23c044 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:49:50 +0530 Subject: [PATCH 2/9] handle unscoped holds correctly --- src/slotify/availability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slotify/availability.py b/src/slotify/availability.py index 9f608fa..8eefd0c 100644 --- a/src/slotify/availability.py +++ b/src/slotify/availability.py @@ -695,7 +695,7 @@ def hold( return self.hold_store.create(BookingHold( slot=slot, expires_at=expires_at, - resource_ids=candidate.effective_resource_ids, + resource_ids=tuple(r for r in candidate.effective_resource_ids if r is not None), metadata=metadata or {}, )) From 4765c2cb0a3144103d8bf069685ac8035021d49b Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:50:05 +0530 Subject: [PATCH 3/9] add final multi-resource hold regression tests --- tests/test_final_audit.py | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_final_audit.py diff --git a/tests/test_final_audit.py b/tests/test_final_audit.py new file mode 100644 index 0000000..1444217 --- /dev/null +++ b/tests/test_final_audit.py @@ -0,0 +1,63 @@ +from datetime import datetime + +import pytest + +from slotify import AvailabilityEngine, BookingConflictError, SlotGenerator + + +def make_generator(): + return SlotGenerator( + start="09:00", + end="12:00", + duration=60, + timezone="Asia/Kolkata", + ) + + +def test_hold_tracks_all_required_resources(): + engine = AvailabilityEngine( + make_generator(), + resource_ids=("room", "therapist"), + ) + slot = make_generator().generate_for_date("2026-10-05")[0] + now = datetime.fromisoformat("2026-10-04T08:00:00+05:30") + hold = engine.hold( + slot, + expires_at=datetime.fromisoformat("2026-10-04T09:00:00+05:30"), + now=now, + ) + assert hold.resource_ids == ("room", "therapist") + assert engine.check_availability(slot, now=now).available is False + with pytest.raises(BookingConflictError): + engine.reserve(slot, now=now) + + +def test_multi_resource_hold_confirm_preserves_resource_requirements(): + engine = AvailabilityEngine( + make_generator(), + resource_ids=("room", "therapist"), + ) + slot = make_generator().generate_for_date("2026-10-05")[0] + now = datetime.fromisoformat("2026-10-04T08:00:00+05:30") + hold = engine.hold( + slot, + expires_at=datetime.fromisoformat("2026-10-04T09:00:00+05:30"), + now=now, + ) + booking = engine.confirm_hold(hold.hold_id, now=now) + assert booking.resource_ids == ("room", "therapist") + assert booking.resource_id == "room" + + +def test_unscoped_hold_still_works(): + engine = AvailabilityEngine(make_generator()) + slot = make_generator().generate_for_date("2026-10-05")[0] + now = datetime.fromisoformat("2026-10-04T08:00:00+05:30") + hold = engine.hold( + slot, + expires_at=datetime.fromisoformat("2026-10-04T09:00:00+05:30"), + now=now, + ) + assert hold.resource_ids == () + with pytest.raises(BookingConflictError): + engine.reserve(slot, now=now) From 165cd9132fb3f6276d108f7167411d2c92939b63 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:51:24 +0530 Subject: [PATCH 4/9] correct multi-resource booking assertion --- tests/test_final_audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_final_audit.py b/tests/test_final_audit.py index 1444217..6c2d009 100644 --- a/tests/test_final_audit.py +++ b/tests/test_final_audit.py @@ -46,7 +46,7 @@ def test_multi_resource_hold_confirm_preserves_resource_requirements(): ) booking = engine.confirm_hold(hold.hold_id, now=now) assert booking.resource_ids == ("room", "therapist") - assert booking.resource_id == "room" + assert booking.resource_id is None def test_unscoped_hold_still_works(): From bc91ba1926a1ee0c4b7d123f4f6bc805702db8e1 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:51:57 +0530 Subject: [PATCH 5/9] prepare final 0.5.1 release metadata --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09850e1..fea5f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "slotify-scheduling" -version = "0.5.0" +version = "0.5.1" description = "Timezone-aware Python scheduling and appointment slot engine for availability and booking systems." readme = "README.md" requires-python = ">=3.10" From 469b915226edc5aaf61efb410377121c7c1ba6a4 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:52:00 +0530 Subject: [PATCH 6/9] prepare final 0.5.1 release metadata --- src/slotify/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slotify/__init__.py b/src/slotify/__init__.py index de5e335..4de1b88 100644 --- a/src/slotify/__init__.py +++ b/src/slotify/__init__.py @@ -36,7 +36,7 @@ to_utc, ) -__version__ = "0.5.0" +__version__ = "0.5.1" __all__ = [ "SlotGenerator", "Slot", From be95ba83192a933825b25ac22ad3de80166948f7 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:52:05 +0530 Subject: [PATCH 7/9] prepare final 0.5.1 release metadata --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1e65be..4f6e4cc 100644 --- a/README.md +++ b/README.md @@ -434,7 +434,7 @@ RecurrenceRule( ## Project status -Current version: **0.5.0** +Current version: **0.5.1** Slotify is currently in alpha. The API may evolve before 1.0.0, so pin the version in production applications and review the changelog when upgrading. From 9427aecb9d4b6d5fb95fff33804a75b5c2403a68 Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:52:08 +0530 Subject: [PATCH 8/9] prepare final 0.5.1 release metadata --- docs/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index a3df99e..aa9f6af 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -16,7 +16,7 @@ python -c "import slotify; print(slotify.__version__)" Expected for this documentation release: ~~~text -0.5.0 +0.5.1 ~~~ ## Run your tests @@ -181,7 +181,7 @@ Slotify is currently in alpha. For production, pin the version that you have tested: ~~~text -slotify-scheduling==0.5.0 +slotify-scheduling==0.5.1 ~~~ Review the changelog before upgrading. From 47180c23e2fc74adc972cbd8ef3c7561f804b95b Mon Sep 17 00:00:00 2001 From: Kalash Gulati Date: Tue, 29 Sep 2026 14:52:14 +0530 Subject: [PATCH 9/9] prepare final 0.5.1 release metadata --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a611a12..a5e77f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.1 + +- Hardened multi-resource temporary holds so all required resources remain blocked through confirmation. +- Added regression coverage for multi-resource and unscoped hold behavior. +- Hardened v0.5 lifecycle edge cases: idempotency reuse, reschedule preservation, booking limits, and active-hold conflicts. + ## 0.5.0 - Added external BusyPeriod conflicts for calendar/application-provided busy intervals.