From 96f1afb721b062a0069f1d60d44a73b3b7e25eb2 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 20 Aug 2026 13:59:31 +0000
Subject: [PATCH 1/2] fix(api): accept reason as alias for cancellationReason
on cancel
PATCH /reservations/:id/cancel only whitelisted cancellationReason, so a
body with reason (the Connect/bulk field name) failed with
"property reason should not exist" (#321). Both fields are now accepted;
cancellationReason wins when both are set.
Co-authored-by: telivity-otaip
---
.../dto/cancel-reservation.dto.spec.ts | 18 +++++++++++++++++
.../reservation/dto/cancel-reservation.dto.ts | 20 ++++++++++++++++++-
.../reservation/reservation.controller.ts | 6 +++++-
.../reservation/reservation.service.ts | 7 ++++---
4 files changed, 46 insertions(+), 5 deletions(-)
create mode 100644 apps/api/src/modules/reservation/dto/cancel-reservation.dto.spec.ts
diff --git a/apps/api/src/modules/reservation/dto/cancel-reservation.dto.spec.ts b/apps/api/src/modules/reservation/dto/cancel-reservation.dto.spec.ts
new file mode 100644
index 00000000..8559d50c
--- /dev/null
+++ b/apps/api/src/modules/reservation/dto/cancel-reservation.dto.spec.ts
@@ -0,0 +1,18 @@
+import { describe, expect, it } from 'vitest';
+import { resolveCancellationReason } from './cancel-reservation.dto';
+
+describe('resolveCancellationReason', () => {
+ it('uses cancellationReason when set', () => {
+ expect(
+ resolveCancellationReason({ cancellationReason: 'guest request', reason: 'ignored' }),
+ ).toBe('guest request');
+ });
+
+ it('falls back to reason alias', () => {
+ expect(resolveCancellationReason({ reason: 'guest request' })).toBe('guest request');
+ });
+
+ it('returns undefined when neither is set', () => {
+ expect(resolveCancellationReason({})).toBeUndefined();
+ });
+});
diff --git a/apps/api/src/modules/reservation/dto/cancel-reservation.dto.ts b/apps/api/src/modules/reservation/dto/cancel-reservation.dto.ts
index be5b1bd2..bcc4fe4e 100644
--- a/apps/api/src/modules/reservation/dto/cancel-reservation.dto.ts
+++ b/apps/api/src/modules/reservation/dto/cancel-reservation.dto.ts
@@ -2,8 +2,26 @@ import { IsString, IsOptional } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class CancelReservationDto {
- @ApiPropertyOptional()
+ @ApiPropertyOptional({
+ description:
+ 'Cancellation reason (preferred). Connect/bulk use `reason`; both are accepted.',
+ })
@IsOptional()
@IsString()
cancellationReason?: string;
+
+ @ApiPropertyOptional({
+ description:
+ 'Alias for cancellationReason (matches Connect/bulk cancel payloads). Ignored when cancellationReason is set.',
+ })
+ @IsOptional()
+ @IsString()
+ reason?: string;
+}
+
+/** Prefer cancellationReason; accept `reason` as alias (#321 item 2). */
+export function resolveCancellationReason(
+ dto: Pick,
+): string | undefined {
+ return dto.cancellationReason ?? dto.reason;
}
diff --git a/apps/api/src/modules/reservation/reservation.controller.ts b/apps/api/src/modules/reservation/reservation.controller.ts
index 17e4c6ac..92e7c549 100644
--- a/apps/api/src/modules/reservation/reservation.controller.ts
+++ b/apps/api/src/modules/reservation/reservation.controller.ts
@@ -300,7 +300,11 @@ export class ReservationController {
@Patch(':id/cancel')
@Roles('admin', 'general_manager', 'front_desk', 'reservations')
- @ApiOperation({ summary: 'Cancel reservation with optional reason' })
+ @ApiOperation({
+ summary: 'Cancel reservation with optional reason',
+ description:
+ 'Optional body field: `cancellationReason` (preferred) or `reason` (alias matching Connect/bulk). Empty body is allowed.',
+ })
@ApiQuery({ name: 'propertyId', required: true })
@ApiResponse({ status: 200, description: 'Reservation cancelled' })
cancelReservation(
diff --git a/apps/api/src/modules/reservation/reservation.service.ts b/apps/api/src/modules/reservation/reservation.service.ts
index 7fd90ea1..c5d0a326 100644
--- a/apps/api/src/modules/reservation/reservation.service.ts
+++ b/apps/api/src/modules/reservation/reservation.service.ts
@@ -24,7 +24,7 @@ import { CreateReservationDto } from './dto/create-reservation.dto';
import { ModifyReservationDto } from './dto/modify-reservation.dto';
import { AssignRoomDto } from './dto/assign-room.dto';
import { MoveRoomDto } from './dto/move-room.dto';
-import { CancelReservationDto } from './dto/cancel-reservation.dto';
+import { CancelReservationDto, resolveCancellationReason } from './dto/cancel-reservation.dto';
import { ListReservationsDto } from './dto/list-reservations.dto';
import { CheckInDto } from './dto/check-in.dto';
import { PreRegisterDto } from './dto/pre-register.dto';
@@ -319,6 +319,7 @@ export class ReservationService {
async cancel(id: string, propertyId: string, dto: CancelReservationDto) {
const reservation = await this.findByIdRaw(id, propertyId);
assertTransition(reservation.status as ReservationStatus, 'cancelled');
+ const cancellationReason = resolveCancellationReason(dto);
// Bug 2: conditional claim prevents double-cancel races. Allowed from
// any pre-check-in state per the state machine.
@@ -329,7 +330,7 @@ export class ReservationService {
{
status: 'cancelled',
cancelledAt: new Date(),
- cancellationReason: dto.cancellationReason,
+ cancellationReason,
updatedAt: new Date(),
},
'cancelled',
@@ -368,7 +369,7 @@ export class ReservationService {
arrivalDate: updated.arrivalDate,
departureDate: updated.departureDate,
roomTypeId: updated.roomTypeId,
- cancellationReason: dto.cancellationReason,
+ cancellationReason,
penaltyAmount: settlement?.penaltyAmount ?? null,
withinFreeWindow: settlement?.withinFreeWindow ?? null,
},
From b2f319a96f693c082ebf6128aebf69a8b0a703f4 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 21 Aug 2026 01:30:40 +0000
Subject: [PATCH 2/2] chore: sync README test counts for cancel-reason alias
specs
CI failed the readme:sync-tests gate (1532/216 vs actual 1535/217)
after cancel-reservation.dto.spec.ts landed on #335.
Co-authored-by: telivity-otaip
---
README.md | 8 ++++----
docs/test-stats.json | 6 +++---
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index fa93b19e..feeb125d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -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 (1532 tests across 216 test files) | Unit and integration tests || Build | tsup (packages) + Vite (dashboard) + nest build (API) | Fast builds |
+| Testing | Vitest (1535 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 (1532 tests across 216 test files)
+# All tests (1535 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 (1532 tests, 216 files)
+pnpm test # Run all tests (1535 tests, 217 files)
pnpm lint # ESLint
```
diff --git a/docs/test-stats.json b/docs/test-stats.json
index 0dca21ad..b8a525ee 100644
--- a/docs/test-stats.json
+++ b/docs/test-stats.json
@@ -1,5 +1,5 @@
{
- "tests": 1532,
- "files": 216,
- "updatedAt": "2026-08-20T16:35:07.748Z"
+ "tests": 1535,
+ "files": 217,
+ "updatedAt": "2026-08-21T01:30:40.178Z"
}