Browse Source

diff: don't report boundary-touching checkouts as added reservations

_diff_reservations excluded local calendar events whose checkin predates
the window ("belongs to a prior period's scope") but applied no matching
filter to the service-side reservations, which touch the same boundary
differently: Booking.com's get_reservations returns any stay whose
checkout lands exactly on start_date, even though it has zero actual
nights in the window. That asymmetry showed up as a spurious 'added'
reservation diff on every month boundary with a checkout on the 1st.

Apply the same start_date >= window-start rule to the service side so
both sides attribute a reservation to the same period.
ZiB 5 days ago
parent
commit
c401196404
2 changed files with 40 additions and 0 deletions
  1. 4 0
      room_cal/actions/diff.py
  2. 36 0
      tests/test_action_chains.py

+ 4 - 0
room_cal/actions/diff.py

@@ -221,6 +221,10 @@ async def _diff_reservations(
       'checkout_date':  r['end_date'],
     }
     for r in service_reservations
+    if r['start_date'] >= start_date
+    # checkin before range — belongs to a prior period's scope, same as cal_by_id above.
+    # Services (e.g. Booking.com) return any stay whose checkout merely touches start_date
+    # (zero actual nights in this window) rather than one that truly overlaps it.
   }
 
   diffs: list[dict] = []

+ 36 - 0
tests/test_action_chains.py

@@ -322,3 +322,39 @@ async def test_diff_returns_complete_structure():
     assert isinstance(diffs, list)
     for row in diffs:
       assert 'date' in row and 'local' in row and 'rule' in row
+
+
+@pytest.mark.asyncio
+async def test_diff_reservations_ignores_boundary_touching_checkout(monkeypatch):
+  """A reservation whose checkout lands exactly on start_date has zero real nights in
+  this window and belongs to the prior period's diff — it must not be reported as
+  'added' just because the service's own range query includes boundary-touching stays
+  (e.g. Booking.com returning a checkout == date_from reservation)."""
+  import room_cal.actions.diff as diff_mod
+
+  class _FakeReservationCal:
+    async def list(self, start, end):
+      return []
+
+  class _ReservationsAPI(_MockAPI):
+    async def get_reservations(self, start, end, *, _out_errors=None):
+      return [
+        {  # touches the boundary only — checkout == start_date
+          'id': 'boundary', 'guest_name': 'Boundary Guest', 'service': 'booking',
+          'start_date': start - timedelta(days=1), 'end_date': start,
+        },
+        {  # genuinely starts inside the window
+          'id': 'real', 'guest_name': 'Real Guest', 'service': 'booking',
+          'start_date': start, 'end_date': start + timedelta(days=2),
+        },
+      ]
+
+  monkeypatch.setattr(diff_mod, 'Reservation', _FakeReservationCal)
+
+  start = date(2026, 10, 1)
+  end = date(2026, 10, 31)
+  result = await diff_mod._diff_reservations(start, end, _ReservationsAPI())
+
+  ids = {row.get('reservation_id') for row in result}
+  assert 'boundary' not in ids
+  assert 'real' in ids