Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ASSUMPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ not.
projection use. A room-less property has no rate; `price_from` renders as `null`.
([ADR-0003](docs/adr/0003-price-filter-on-cheapest-room.md))

- **A room-less property satisfies no price bound.** Having no bookable rate, it is
excluded from *any* `min_price`/`max_price` result — symmetrically on both sides of the
band, not just the ceiling. The current seed has no room-less properties, so this is a
defensive-path guarantee rather than an observable case today.
([ADR-0003](docs/adr/0003-price-filter-on-cheapest-room.md))

- **Price filtering is independent of availability.** `GET /hotels` is a discovery
surface — "what exists in my budget" — and returns a property regardless of whether its
rooms are open on any particular dates. `/hotels` doesn't even take dates; date-based
Expand Down
1 change: 1 addition & 0 deletions docs/adr/0003-price-filter-on-cheapest-room.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ The brief asks to filter properties by price, but a property has no price of its
- A property whose only room has no open dates still appears in price-filtered `/hotels` results, because it still has a listed nightly rate. Availability is resolved later, per-property, on the rooms endpoint.
- `price_from` is also returned in the list projection, so a client can display "from $X" without a second call.
- `min_price > max_price` is contradictory input → `400`.
- A room-less property has no `price_from` (the primitive yields `Infinity`), so it has no price to satisfy *any* bound and is excluded from every price-filtered result — symmetrically on both the `min_price` floor and the `max_price` ceiling, not just the ceiling. (The seed has no room-less properties; this is a defensive-path guarantee.)
4 changes: 4 additions & 0 deletions src/domain/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export function filterHotels(properties: Property[], filters: HotelFilters): Pro

if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
const price = priceFrom(property);
// A room-less Property has no bookable rate (`priceFrom` yields Infinity),
// so it has no price to satisfy *any* bound — exclude it from every
// price-filtered result, symmetrically on both sides of the band.
if (!Number.isFinite(price)) return false;
if (filters.minPrice !== undefined && price < filters.minPrice) return false;
if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
}
Expand Down
7 changes: 6 additions & 1 deletion test/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,16 @@ describe('priceFrom', () => {
);
});

it('yields Infinity for a Property with no Rooms, so it never satisfies a max_price', () => {
it('yields Infinity for a Property with no Rooms, so it satisfies neither price bound', () => {
const roomless = makeProperty({ roomPrices: [] });

expect(priceFrom(roomless)).toBe(Infinity);
// A Property with no bookable rate has no price, so it must be excluded by
// any price filter — the max_price ceiling *and* the min_price floor.
expect(filterHotels([roomless], { maxPrice: 1000 })).toEqual([]);
expect(filterHotels([roomless], { minPrice: 1000 })).toEqual([]);
expect(filterHotels([roomless], { minPrice: 0 })).toEqual([]);
expect(filterHotels([roomless], { minPrice: 100, maxPrice: 500 })).toEqual([]);
});
});

Expand Down
Loading