From f9cc7f2416ea960744d2784b1b72945ab191ffe1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 03:31:18 +0000 Subject: [PATCH 1/2] fix(reports): occupancy and roomsSold use exclusive departure getOccupancy occupied/stayovers and financial-summary roomsSold now require departure > report date, matching occupancy-trend and calendar room-nights, so sticky room.status / past-departure stayovers no longer inflate tonight occupancy. Co-authored-by: telivity-otaip --- .../modules/reports/reports.service.spec.ts | 33 +++++++++++++++++++ .../src/modules/reports/reports.service.ts | 27 ++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/api/src/modules/reports/reports.service.spec.ts b/apps/api/src/modules/reports/reports.service.spec.ts index abee49a9..a98d4b9d 100644 --- a/apps/api/src/modules/reports/reports.service.spec.ts +++ b/apps/api/src/modules/reports/reports.service.spec.ts @@ -151,6 +151,8 @@ describe('ReportsService', () => { { status: 'out_of_order', count: 5 }, { status: 'out_of_service', count: 5 }, ], + // occupied stay-window count (departure exclusive) + [{ count: 60 }], // arrivals [{ count: 8 }], // departures @@ -191,6 +193,7 @@ describe('ReportsService', () => { { status: 'out_of_order', count: 10 }, { status: 'out_of_service', count: 5 }, ], + [{ count: 20 }], // occupied stay-window [{ count: 0 }], [{ count: 0 }], [{ count: 0 }], [{ count: 0 }], [{ count: 0 }], ]); const module = await Test.createTestingModule({ @@ -207,6 +210,36 @@ describe('ReportsService', () => { expect(result.occupancyRate).toBeCloseTo(0.5714, 3); }); + it('should ignore sticky room.status when stay window has no guests', async () => { + const db = createMockDb([ + [{ totalRooms: 10 }], + [ + { status: 'occupied', count: 7 }, + { status: 'out_of_order', count: 1 }, + { status: 'vacant_clean', count: 2 }, + ], + [{ count: 0 }], // stay-window occupied = 0 (stale stayovers past departure) + [{ count: 0 }], + [{ count: 0 }], + [{ count: 0 }], + [{ count: 0 }], + [{ count: 0 }], + ]); + const module = await Test.createTestingModule({ + providers: [ + ReportsService, + { provide: DRIZZLE, useValue: db }, + ], + }).compile(); + service = module.get(ReportsService); + + const result = await service.getOccupancy('prop-001', '2026-08-20'); + expect(result.occupiedRooms).toBe(0); + expect(result.availableRooms).toBe(9); + expect(result.occupancyRate).toBe(0); + expect(result.stayovers).toBe(0); + }); + // --- Financial Summary --- it('should calculate ADR correctly (revenue / rooms sold)', async () => { diff --git a/apps/api/src/modules/reports/reports.service.ts b/apps/api/src/modules/reports/reports.service.ts index f1b6eafd..eda8da43 100644 --- a/apps/api/src/modules/reports/reports.service.ts +++ b/apps/api/src/modules/reports/reports.service.ts @@ -139,14 +139,29 @@ export class ReportsService { let outOfOrder = 0; let outOfService = 0; - let occupiedRooms = 0; + let occupiedFromRoomStatus = 0; for (const row of roomStatusCounts) { if (row.status === 'out_of_order') outOfOrder = row.count; else if (row.status === 'out_of_service') outOfService = row.count; - else if (row.status === 'occupied') occupiedRooms = row.count; + else if (row.status === 'occupied') occupiedFromRoomStatus = row.count; } const availableRooms = totalRooms - outOfOrder - outOfService; + + // Room-nights occupied on reportDate (departure exclusive). Prefer stay window + // over sticky room.status so stale demo stayovers past departure do not inflate %. + const [occupiedStayResult] = await this.db + .select({ count: sql`count(*)::int` }) + .from(reservations) + .where( + and( + eq(reservations.propertyId, propertyId), + sql`${reservations.status} in ('checked_in', 'stayover', 'due_out', 'checked_out')`, + lte(reservations.arrivalDate, reportDate), + sql`${reservations.departureDate} > ${reportDate}`, + ), + ); + const occupiedRooms = occupiedStayResult?.count ?? occupiedFromRoomStatus; const occupancyRate = availableRooms > 0 ? occupiedRooms / availableRooms : 0; // Arrivals (checked in today) @@ -171,7 +186,7 @@ export class ReportsService { ), ); - // Stayovers (in-house continuing) + // Stayovers — in-house continuing on reportDate (same stay-window as occupied) const [stayoversResult] = await this.db .select({ count: sql`count(*)::int` }) .from(reservations) @@ -180,6 +195,7 @@ export class ReportsService { eq(reservations.propertyId, propertyId), sql`${reservations.status} in ('stayover', 'checked_in', 'due_out')`, lte(reservations.arrivalDate, reportDate), + sql`${reservations.departureDate} > ${reportDate}`, ), ); @@ -303,15 +319,16 @@ export class ReportsService { paymentsByMethod[row.method] = new Decimal(row.total).toNumber(); } - // Rooms sold + // Rooms sold — room-nights covering `date` (departure exclusive; matches occupancy trend) const [roomsSoldResult] = await this.db .select({ count: sql`count(*)::int` }) .from(reservations) .where( and( eq(reservations.propertyId, propertyId), - sql`${reservations.status} in ('checked_in', 'stayover', 'due_out')`, + sql`${reservations.status} in ('checked_in', 'stayover', 'due_out', 'checked_out')`, lte(reservations.arrivalDate, date), + sql`${reservations.departureDate} > ${date}`, ), ); const roomsSold = roomsSoldResult?.count ?? 0; From 398962e66caf4a07ff8a83ea63ead06fba8da9d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 03:39:58 +0000 Subject: [PATCH 2/2] chore: sync README test counts to 1536 PR added one reports.service stay-window test; CI check requires README/docs/test-stats.json to match the suite count. Co-authored-by: telivity-otaip --- README.md | 8 ++++---- docs/test-stats.json | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index feeb125d..61b64623 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ NestJS PostgreSQL Apache 2.0 License -1535 Tests Passing 12 AI Agents +1536 Tests Passing 12 AI Agents

@@ -510,7 +510,7 @@ Operator notes for activating existing adapters, metasearch landings on the dire | OTA Channels | Booking.com + Expedia (EQC) + SiteMinder + DerbySoft | Direct + aggregated OTA connectivity (ARI + content) | | XML Processing | fast-xml-parser | Booking.com OTA XML protocol | | Package Manager | pnpm workspaces | Monorepo management | -| Testing | Vitest (1535 tests across 217 test files) | Unit and integration tests || Build | tsup (packages) + Vite (dashboard) + nest build (API) | Fast builds | +| Testing | Vitest (1536 tests across 217 test files) | Unit and integration tests || Build | tsup (packages) + Vite (dashboard) + nest build (API) | Fast builds | | Containers | Docker + docker-compose | Local dev and production deployment | | CI/CD | GitHub Actions | Automated testing, builds, and releases | @@ -642,7 +642,7 @@ Before going live, verify the items in [`docs/deployment.md`](./docs/deployment. ### Run tests ```bash -# All tests (1535 tests across 217 test files) +# All tests (1536 tests across 217 test files) # API tests only pnpm --filter @telivityhaip/api test @@ -1190,7 +1190,7 @@ HAIP is built in public and contributions are welcome. pnpm install # Install dependencies pnpm build # Build all workspace packages pnpm dev # Start API in dev mode (hot reload) -pnpm test # Run all tests (1535 tests, 217 files) +pnpm test # Run all tests (1536 tests, 217 files) pnpm lint # ESLint ``` diff --git a/docs/test-stats.json b/docs/test-stats.json index b8a525ee..450c22e6 100644 --- a/docs/test-stats.json +++ b/docs/test-stats.json @@ -1,5 +1,6 @@ { - "tests": 1535, + "tests": 1536, "files": 217, - "updatedAt": "2026-08-21T01:30:40.178Z" + "updatedAt": "2026-08-21T03:39:58.360" } +Z \ No newline at end of file