Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7d46321
Add opt-in booking-requests package with core seams and UI gates
cursoragent Aug 26, 2026
827df43
refactor(booking-requests): slim package PR to booking-requests scope
cursoragent Aug 26, 2026
bb82870
fix(ci): restore core seams, push-schema columns, and README test counts
cursoragent Aug 26, 2026
3a99aa2
fix(ci): add charge amendment columns to push-schema and webhook spec
cursoragent Aug 26, 2026
ee16f70
fix(docker): lazy-load booking-requests only when feature flag is on
cursoragent Aug 26, 2026
5b6a37e
fix(docker): preload optional booking-requests modules async
cursoragent Aug 26, 2026
4c5a3ae
fix(api): type bootstrap cache as DynamicModule array
cursoragent Aug 26, 2026
c7be322
Merge origin/main into cursor/booking-requests-package-4a4f
cursoragent Aug 27, 2026
99ed4f2
Merge origin/main into cursor/booking-requests-package-4a4f
cursoragent Aug 27, 2026
61c86b3
fix(booking-requests): address Agustin packaging/safety review
cursoragent Aug 27, 2026
2599519
fix(database): keep logical_event_id out of push-schema baseline
cursoragent Aug 27, 2026
e0e9d20
chore: sync README test counts to 2180/260
cursoragent Aug 27, 2026
bb761dd
test(booking-requests): flag-off regression gate + port PR #347 safet…
cursoragent Aug 27, 2026
1760c90
feat(booking-requests): package-owned port for booking_mode/payment_m…
cursoragent Aug 27, 2026
0760ea4
chore(database): remove request-only DDL from core push-schema/drizzle
cursoragent Aug 27, 2026
43f6c23
refactor(booking-requests): move Nest vertical slice into package
cursoragent Aug 27, 2026
05e6b31
fix(docker): ship shared node_modules for Nest peer in prod image
cursoragent Aug 27, 2026
e71537b
fix(api): load AppModule after booking-requests preload
cursoragent Aug 28, 2026
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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ NODE_ENV=development
# SERVE_DASHBOARD=true # dashboard at /
# SERVE_BOOKING=true # booking engine at /booking/

# Optional booking-requests module (STR / request-first direct booking).
# When false (default), request-mode tables, routes, and Stripe handlers are not loaded.
# HAIP_BOOKING_REQUESTS=false

# Dashboard / booking widget: set true when the API runs with HAIP_BOOKING_REQUESTS=true
# VITE_HAIP_BOOKING_REQUESTS=false

# In production the API refuses to boot with an insecure config
# (AUTH_ENABLED=false or STRIPE_MODE=mock) to prevent an accidental insecure
# real deployment. The intentional public demo sets this to opt out.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- run: pnpm install --frozen-lockfile

- name: Build packages
run: pnpm -r --filter @telivityhaip/shared --filter @telivityhaip/database run build
run: pnpm -r --filter @telivityhaip/shared --filter @telivityhaip/database --filter @telivityhaip/booking-requests run build

- name: Lint
run: pnpm lint
Expand Down
65 changes: 64 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,71 @@ jobs:
DATABASE_URL: postgresql://haip:haip@localhost:5432/haip_test
REDIS_URL: redis://localhost:6379

ci-booking-requests:
name: CI (booking-requests)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: haip
POSTGRES_PASSWORD: haip
POSTGRES_DB: haip_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'

- run: pnpm install --frozen-lockfile

- name: Build all packages
run: pnpm build

- name: Push core database schema
run: pnpm db:migrate
env:
DATABASE_URL: postgresql://haip:haip@localhost:5432/haip_test

- name: Push booking-requests schema
run: pnpm db:migrate:booking-requests
env:
DATABASE_URL: postgresql://haip:haip@localhost:5432/haip_test

- name: Run booking-requests release gate
run: pnpm --filter @telivityhaip/api exec vitest run src/modules/booking-request/booking-request-default-flow-regression.spec.ts
env:
DATABASE_URL: postgresql://haip:haip@localhost:5432/haip_test
REDIS_URL: redis://localhost:6379
HAIP_BOOKING_REQUESTS: 'true'
AUTH_ENABLED: 'false'

release:
name: Auto Release
needs: ci
needs: [ci, ci-booking-requests]
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.version.outputs.skip }}
Expand Down Expand Up @@ -205,6 +267,7 @@ jobs:
VITE_KEYCLOAK_URL=http://localhost:8080
VITE_KEYCLOAK_REALM=haip
VITE_KEYCLOAK_CLIENT_ID=haip-dashboard
VITE_HAIP_BOOKING_REQUESTS=false
tags: |
ghcr.io/telivityai/haip-api:${{ needs.release.outputs.next }}
ghcr.io/telivityai/haip-api:latest
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-1635%20passing-brightgreen" alt="1635 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-2222%20passing-brightgreen" alt="2222 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 (1635 passing tests across 229 files with passing tests) | Unit and integration tests |
| Testing | Vitest (2222 passing tests across 266 files with passing tests) | 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 @@ -643,7 +643,7 @@ Before going live, verify the items in [`docs/deployment.md`](./docs/deployment.
### Run tests

```bash
# Passing-test count: 1635 test cases across 229 files (skipped excluded)
# Passing-test count: 2222 test cases across 266 files (skipped excluded)

# API tests only
pnpm --filter @telivityhaip/api test
Expand Down Expand Up @@ -1191,7 +1191,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 (1635 passing, 229 files with passes; skipped excluded)
pnpm test # Run all tests (2222 passing, 266 files with passes; skipped excluded)
pnpm lint # ESLint
```

Expand Down
21 changes: 19 additions & 2 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ COPY apps/dashboard/package.json ./apps/dashboard/
COPY apps/booking/package.json ./apps/booking/
COPY packages/database/package.json ./packages/database/
COPY packages/shared/package.json* ./packages/shared/
COPY packages/booking-requests/package.json ./packages/booking-requests/

RUN pnpm install --frozen-lockfile || pnpm install

Expand All @@ -24,16 +25,21 @@ COPY apps/booking/ ./apps/booking/
# Build workspace packages
RUN pnpm --filter @telivityhaip/shared run build
RUN pnpm --filter @telivityhaip/database run build
RUN pnpm --filter @telivityhaip/booking-requests run build

# Dashboard SPA — auth/Keycloak settings are baked at build time (Vite env).
# Dashboard/booking SPAs — auth/Keycloak/feature flags are baked at build time (Vite env).
ARG VITE_AUTH_ENABLED=false
ARG VITE_KEYCLOAK_URL=http://localhost:8080
ARG VITE_KEYCLOAK_REALM=haip
ARG VITE_KEYCLOAK_CLIENT_ID=haip-dashboard
# Must match the API's HAIP_BOOKING_REQUESTS at deploy time, or the module can
# be enabled server-side while the UIs compile it out (see booking-engine-config.service.ts).
ARG VITE_HAIP_BOOKING_REQUESTS=false
ENV VITE_AUTH_ENABLED=$VITE_AUTH_ENABLED \
VITE_KEYCLOAK_URL=$VITE_KEYCLOAK_URL \
VITE_KEYCLOAK_REALM=$VITE_KEYCLOAK_REALM \
VITE_KEYCLOAK_CLIENT_ID=$VITE_KEYCLOAK_CLIENT_ID
VITE_KEYCLOAK_CLIENT_ID=$VITE_KEYCLOAK_CLIENT_ID \
VITE_HAIP_BOOKING_REQUESTS=$VITE_HAIP_BOOKING_REQUESTS

# Build dashboard (Vite static output)
RUN pnpm --filter dashboard run build
Expand Down Expand Up @@ -65,6 +71,17 @@ COPY --from=base /app/packages/database/package.json ./packages/database/
COPY --from=base /app/packages/database/node_modules ./packages/database/node_modules
COPY --from=base /app/packages/shared/dist ./packages/shared/dist
COPY --from=base /app/packages/shared/package.json ./packages/shared/
# shared now peers on @nestjs/common (access decorators / HTTP exceptions
# relocated here for the booking-requests package boundary). Without this
# copy, `require('@nestjs/common')` from packages/shared/dist fails in the
# production image even though root node_modules is present — pnpm keeps the
# peer under packages/shared/node_modules.
COPY --from=base /app/packages/shared/node_modules ./packages/shared/node_modules
COPY --from=base /app/packages/booking-requests/dist ./packages/booking-requests/dist
COPY --from=base /app/packages/booking-requests/package.json ./packages/booking-requests/
# booking-requests' own node_modules (postgres) — needed so `node
# dist/database/migrate.js` can run standalone, matching packages/database.
COPY --from=base /app/packages/booking-requests/node_modules ./packages/booking-requests/node_modules

ENV NODE_ENV=production

Expand Down
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"lint": "eslint src/",
"typecheck": "tsc --noEmit -p tsconfig.build.json",
"test": "vitest run",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:release-smoke": "RELEASE_SMOKE=1 vitest run src/release-smoke.integration.spec.ts",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
Expand All @@ -31,6 +32,7 @@
"@nestjs/websockets": "^10.0.0",
"@telivityhaip/database": "workspace:*",
"@telivityhaip/shared": "workspace:^",
"@telivityhaip/booking-requests": "workspace:*",
"@types/jsonwebtoken": "^9.0.10",
"bullmq": "^5.81.1",
"class-transformer": "^0.5.1",
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,18 @@ import { LoyaltyModule } from './modules/loyalty/loyalty.module';
import { IntegrationsModule } from './modules/integrations/integrations.module';
import { IcalModule } from './modules/ical/ical.module';
import { FiscalModule } from './modules/fiscal/fiscal.module';
import { bookingRequestsModules } from './booking-requests.bootstrap';

const imports: any[] = [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
EventEmitterModule.forRoot(),
// `wildcard: true` is required for ConnectEventsService/EventsService's
// `@OnEvent('**')` catch-all listeners (webhook fan-out + the live dashboard
// event feed) to receive every emitted event — eventemitter2 defaults to
// `wildcard: false`, under which '**' listeners never match anything.
EventEmitterModule.forRoot({ wildcard: true, delimiter: '.' }),
DatabaseModule,
HealthModule,
PropertyModule,
Expand All @@ -70,6 +75,7 @@ const imports: any[] = [
FolioModule,
RatePlanModule,
PaymentModule,
...bookingRequestsModules(),
HousekeepingModule,
LostAndFoundModule,
ServiceRequestsModule,
Expand Down
117 changes: 117 additions & 0 deletions apps/api/src/booking-requests.bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { DynamicModule, Type } from '@nestjs/common';

let cachedModules: Array<Type | DynamicModule> | null | undefined;

/**
* Builds the `@telivityhaip/booking-requests` package's `BookingRequestModule.forRoot(...)`
* DynamicModule, binding every port it declares to the concrete core singleton
* that satisfies it (mostly `useExisting`). This is the ONLY place core wires
* itself to the optional package — the package itself never imports from
* `apps/api`.
*/
async function buildBookingRequestsModule(): Promise<DynamicModule> {
const [
{ BookingRequestModule },
{ AncillaryModule },
{ AncillaryService },
{ BookingEngineModule },
{ BookingEngineService },
{ BookingEngineConfigService },
{ BookingKeyGuard },
{ BookingEngineScopeGuard },
{ BookingThrottleGuard },
{ EmailModule },
{ EmailService },
{ FolioModule },
{ FolioService },
{ GuestModule },
{ GuestService },
{ PaymentModule },
{ RatePlanModule },
{ RatePlanService },
{ ReservationModule },
{ ReservationService },
{ AvailabilityService },
{ WebhookModule },
{ WebhookService },
] = await Promise.all([
import('@telivityhaip/booking-requests'),
import('./modules/ancillary/ancillary.module.js'),
import('./modules/ancillary/ancillary.service.js'),
import('./modules/booking-engine/booking-engine.module.js'),
import('./modules/booking-engine/booking-engine.service.js'),
import('./modules/booking-engine/booking-engine-config.service.js'),
import('./modules/auth/booking-key.guard.js'),
import('./modules/auth/booking-engine-scope.guard.js'),
import('./modules/booking-engine/booking-throttle.guard.js'),
import('./modules/agent/guest-comms/email.module.js'),
import('./modules/agent/guest-comms/email.service.js'),
import('./modules/folio/folio.module.js'),
import('./modules/folio/folio.service.js'),
import('./modules/guest/guest.module.js'),
import('./modules/guest/guest.service.js'),
import('./modules/payment/payment.module.js'),
import('./modules/rate-plan/rate-plan.module.js'),
import('./modules/rate-plan/rate-plan.service.js'),
import('./modules/reservation/reservation.module.js'),
import('./modules/reservation/reservation.service.js'),
import('./modules/reservation/availability.service.js'),
import('./modules/webhook/webhook.module.js'),
import('./modules/webhook/webhook.service.js'),
]);

return BookingRequestModule.forRoot({
imports: [
AncillaryModule,
BookingEngineModule,
EmailModule,
FolioModule,
GuestModule,
// PaymentModule is required not only for FolioService's payment side
// effects but so SAVED_PAYMENT_METHOD_GATEWAY / PAYMENT_GATEWAY — the
// exact same @telivityhaip/shared tokens on both sides of the package
// boundary — resolve without this module re-binding them.
PaymentModule,
RatePlanModule,
ReservationModule,
WebhookModule,
],
ancillaryService: { useExisting: AncillaryService },
availabilityService: { useExisting: AvailabilityService },
bookingEngineService: { useExisting: BookingEngineService },
bookingEngineConfigService: { useExisting: BookingEngineConfigService },
emailService: { useExisting: EmailService },
folioService: { useExisting: FolioService },
guestService: { useExisting: GuestService },
ratePlanService: { useExisting: RatePlanService },
reservationService: { useExisting: ReservationService },
webhookService: { useExisting: WebhookService },
// Core's BookingEngineController/AuthModule own credential, scope, and
// rate-limit enforcement — bind the package's public-controller guard
// ports to those same singletons instead of duplicating the logic.
bookingKeyGuard: { useExisting: BookingKeyGuard },
bookingEngineScopeGuard: { useExisting: BookingEngineScopeGuard },
bookingThrottleGuard: { useExisting: BookingThrottleGuard },
});
}

/** Preload the optional booking-requests Nest module when the feature flag is on. */
export async function preloadBookingRequestsModules(): Promise<void> {
if (process.env['HAIP_BOOKING_REQUESTS'] !== 'true') {
cachedModules = null;
return;
}
if (cachedModules !== undefined) return;

cachedModules = [await buildBookingRequestsModule()];
}

export function bookingRequestsModules(): Array<Type | DynamicModule> {
if (process.env['HAIP_BOOKING_REQUESTS'] !== 'true') return [];
if (!cachedModules) {
throw new Error(
'Booking requests is enabled but modules were not preloaded — call preloadBookingRequestsModules() before bootstrapping Nest',
);
}
return cachedModules;
}
Loading
Loading