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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/NestJS-framework-E0234E?logo=nestjs&logoColor=white" alt="NestJS" />
<img src="https://img.shields.io/badge/PostgreSQL-database-4169E1?logo=postgresql&logoColor=white" alt="PostgreSQL" />
<img src="https://img.shields.io/badge/License-Apache%202.0-blue" alt="Apache 2.0 License" />
<img src="https://img.shields.io/badge/Tests-1532%20passing-brightgreen" alt="1532 Tests Passing" /> <img src="https://img.shields.io/badge/AI%20Agents-12%20built--in-blueviolet" alt="12 AI Agents" />
<img src="https://img.shields.io/badge/Tests-1535%20passing-brightgreen" alt="1535 Tests Passing" /> <img src="https://img.shields.io/badge/AI%20Agents-12%20built--in-blueviolet" alt="12 AI Agents" />
</p>

<p align="center">
Expand Down Expand Up @@ -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 |

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
20 changes: 19 additions & 1 deletion apps/api/src/modules/reservation/dto/cancel-reservation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CancelReservationDto, 'cancellationReason' | 'reason'>,
): string | undefined {
return dto.cancellationReason ?? dto.reason;
}
6 changes: 5 additions & 1 deletion apps/api/src/modules/reservation/reservation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,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(
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/modules/reservation/reservation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -329,7 +330,7 @@ export class ReservationService {
{
status: 'cancelled',
cancelledAt: new Date(),
cancellationReason: dto.cancellationReason,
cancellationReason,
updatedAt: new Date(),
},
'cancelled',
Expand Down Expand Up @@ -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,
},
Expand Down
6 changes: 3 additions & 3 deletions docs/test-stats.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading