Skip to content

PTX Channel Manager — System Knowledge

Version: 2.1.0 | Date: 2026-02-20


1. What Is PTX-CM?

PTX-CM is an internal OTA extranet automation tool that prevents overbookings by auto-syncing room availability across 4 OTA partner dashboards (Booking.com, Agoda, Traveloka, Expedia). It manages 100+ properties across Vietnam, Indonesia, Malaysia for a hospitality operator.

The Problem

Staff manually update 4 OTA extranets per property (~400+ sessions). When a booking arrives on one OTA, availability isn't reduced on others fast enough → daily overbookings, guest trust damage, OTA penalties.

The Solution

A unified dashboard that:

  1. Connects OTA accounts & auto-discovers properties
  2. Polls OTA extranets every 2-3 min for new bookings
  3. Auto-pushes updated availability to all connected OTAs
  4. Alerts on sync failures or overbookings
  5. Single view of all bookings/availability, country-filtered

2. Architecture

Monorepo Structure

ptx-cm/
├── apps/
│   ├── api/            # NestJS 10 backend (:3002) — REST API /api/v1
│   └── web/            # Next.js 16 frontend (:3100) — App Router
├── packages/
│   ├── database/       # Prisma 7 schema + generated client
│   ├── types/          # Shared TypeScript types & enums
│   └── config/         # ESLint/TS shared configs
├── docs/               # IPA docs (SRD, API_SPEC, DB_DESIGN, UI_SPEC)
├── plans/              # Implementation plan archives
└── prototypes/         # HTML mockups

Tech Stack

LayerTechnology
FrontendNext.js 16, React 18, Tailwind CSS, TanStack Table, react-hook-form, zod, SWR
BackendNestJS 10, Passport JWT, class-validator, BullMQ
DatabasePostgreSQL 16, Prisma 7 ORM (22 models)
Queue/CacheRedis 7, BullMQ job queues
AuthJWT access (15m) + refresh (7d), HttpOnly cookies, bcryptjs
EncryptionAES-256-GCM (OTA credentials)
BuildTurborepo, pnpm workspaces, TypeScript 5.7

Data Flow

Browser → Next.js (:3100) → /api/[...proxy] → NestJS (:3002) → PostgreSQL (:5433)
                                                              → Redis (:6379)
                                                              → OTA Extranets (stubs)

3. Current State (as of 2026-02-20)

Backend Modules (25 total)

ModuleStatusPurpose
authJWT + refresh tokens, login/logout/forgot/reset password
dashboardKPI cards, sync status, recent bookings
propertiesCRUD for hotel properties
room-typesRoom inventory per property
ota-accounts⚠️CRUD + encrypted creds. Test/refresh are stubs
ota-connectionsLink properties ↔ OTA accounts
room-mappingsMap internal rooms to OTA room/rate IDs
bookingsList/detail/export. Status transitions via workflow
booking-statusBookingStatusDef CRUD
booking-status-transitionWorkflow transitions CRUD
booking-hooksaudit_log, update_availability, send_notification hooks
alertsCreate/resolve overbooking & sync failure alerts
sync-jobsJob history, force-sync endpoint
sync-engine⚠️BullMQ orchestration ✅. OTA adapters are all stubs
ota-adapters⚠️4 adapters defined, all return empty/false
settingsGlobal config (sync intervals, notification, SMTP)
usersCRUD + password reset (temp + email link)
roles7 preset roles + custom, bitwise permissions
suppliersCRUD + CSV import/export
supplier-room-allocationsM:N Supplier ↔ RoomType with room count
countriesVN/ID/MY with pill colors, sort order
notificationsEmail via SMTP fallback chain. LINE Notify stub
activity-logsHTTP request logging, super admin only
healthLiveness probe
prismaPrisma service provider

Frontend Pages

RouteScreenStatus
/loginLogin
/dashboardDashboard + activity log
/propertiesProperties list
/properties/[id]Property detail (rooms, OTA connections)
/bookingsBookings list (filters, search, export)
/bookings/[id]Booking detail (status transitions, audit)
/ota-accountsOTA accounts list
/ota-accounts/[id]OTA account detail⚠️
/suppliersSupplier list (import/export CSV)
/suppliers/[id]Supplier detail + room allocations
/alertsAlert list + resolve
/sync-jobsSync job log
/logsActivity/client event logs
/settings7 tabs: Users, Roles, Booking Statuses, Workflow, Prefs, Notifications, System
/profileUser profile + password change

Frontend Contexts & Hooks

Context/HookPurpose
AuthContextUser session, permissions, JWT hydration
CountryContextCountry filter (localStorage persistence)
ThemeContextDark/light mode toggle
ReferenceDataContextShared reference data (countries, roles)
ActivityTrackerProviderClient-side activity tracking
useApiSWR-based data fetching hook
usePermissionPermission checking hook

4. Key Domain Concepts

OTA Account vs OTA Connection

  • OTA Account = one set of credentials for one OTA (e.g., "Booking.com Vietnam North"). Owns the session, encrypted creds, 2FA config.
  • OTA Connection = lightweight link: Property ↔ OTA Account + OTA's property ID. Many properties share one account.

Country Scoping

  • Staff users (country != null) → auto-scoped to their country on all queries
  • Manager/Admin (country = null) → sees all countries, can filter with ?country=XX
  • Frontend uses CountryContext (localStorage-backed) for persistent country filter

Permission Model (Bitwise JSONB)

Roles stored in DB with permissions JSONB field: { "module_name": bitmask }.

  • Bitmask: VIEW=1, CREATE=2, EDIT=4, DELETE=8
  • Checked via @RequirePermission('MODULE', 'EDIT') decorator
  • 7 preset roles: super_admin, admin, manager, ota, cs, fin, po

Booking Status Workflow Engine

  • BookingStatusDef — configurable statuses with color, icon, sort
  • BookingStatusTransition — state machine transitions with:
    • allowedRoles (JSONB array)
    • hooks (JSONB array: audit_log, update_availability, send_notification)
    • uiConfig per role (sections, buttons, editable fields)

Supplier Room Allocation

  • Supplier — owner of apartment rooms (code, name, bank details, contact)
  • SupplierRoomAllocation — M:N junction: Supplier ↔ RoomType with roomCount
  • Soft warning when SUM(allocations) != totalRooms (not blocked)
  • Accounting-only — zero impact on OTA sync

5. Database (22 Models)

Core Entities

ModelTableKey Purpose
RolerolesNamed roles with JSONB permissions
UserusersStaff accounts with country, role FK, date format
RefreshTokenrefresh_tokensJWT revocation tracking
PasswordResetTokenpassword_reset_tokensSHA-256 hashed reset tokens
CountrycountriesVN/ID/MY with pill colors

Property Management

ModelTableKey Purpose
PropertypropertiesHotel properties (country, timezone, currency)
RoomTyperoom_typesRoom categories per property
SuppliersuppliersRoom owners (code, contact, bank info)
SupplierRoomAllocationsupplier_room_allocationsM:N Supplier↔RoomType with room count

OTA Integration

ModelTableKey Purpose
OtaAccountota_accountsEncrypted OTA credentials + session
OtaConnectionota_connectionsProperty ↔ OTA account link
OtaRoomMappingota_room_mappingsInternal room ↔ OTA room/rate plan

Operations

ModelTableKey Purpose
BookingbookingsReservations from OTAs
BookingStatusDefbooking_status_defConfigurable booking statuses
BookingStatusTransitionbooking_status_transitionState machine transitions
AvailabilityavailabilityDaily room availability per room type
SyncJobsync_jobsAsync job tracking (BullMQ)
RateratesDaily rate per room type per OTA
RateRulerate_rulesMarkup/discount/seasonal rules
AlertalertsOverbooking & sync failure notifications
AuditLogaudit_logsEntity change tracking
SettingssettingsSingleton global configuration

Enums

OtaType, ConnectionStatus, TwoFactorMethod, SyncJobType, SyncJobStatus, AlertType, AlertSeverity, AuditAction, RateRuleType


6. Key Patterns

Auth Flow

  1. POST /auth/login → JWT access (15m cookie) + refresh (7d cookie)
  2. Frontend interceptor: 401 → refresh queue → retry. All concurrent 401s wait for single refresh.
  3. Logout → revoke jti in DB → clear cookies

API Proxy

Frontend app/api/[...proxy]/route.ts forwards all /api/* calls to NestJS :3002, forwarding cookies.

SWR Caching

All GET requests use SWR (stale-while-revalidate). Mutations call mutate() to revalidate.
Tab visibility pause prevents background polling.

Soft Deletes

All entities use isActive flag. No hard deletes (bookings, properties, suppliers, etc.)

Audit Trail

AuditLog records all create/update/delete mutations on entities. AuditLogInterceptor handles this.


7. What's NOT Built Yet

FeatureNotes
Real OTA adaptersAll 4 adapters are stubs — no HTTP/scraping
Property discoveryfetchProperties() returns empty
Availability calendar (S-10)No route, no component
Rate manager (S-11)No service, no API, no UI
Booking timeline (S-12)No route, no component
Cancellation syncNo cancelBooking in adapter interface
Rate parity checkerNot built
Revenue analyticsNot built
LINE NotifyStub only
Force Sync UI buttonBackend exists, no frontend button

8. Development Workflow

Commands

bash
pnpm dev          # Start both apps (Turborepo)
pnpm build        # Production build
pnpm db:generate  # Generate Prisma client
pnpm db:migrate   # Run migrations
pnpm db:seed      # Seed sample data
pnpm db:studio    # Prisma Studio (DB browser)

Infrastructure (Docker)

bash
docker compose up -d  # PostgreSQL 16 (:5433) + Redis 7 (:6379) + Mailpit (:8025)

Custom Workflows

  • /git-push — Commit and push via Gitea
  • /schema-change — Modify Prisma schema safely
  • /wsl-run — Run pnpm/node via WSL

PTX Channel Manager — Tài Liệu Nội Bộ