Skip to content

PTX Channel Manager — System Knowledge

Version: 3.0.5 | Date: 2026-06-05


1. What Is PTX-CM?

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

The Problem

Staff manually update multiple 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/         # Shared tsconfig presets
├── 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 (30 models, 9 enums)
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-06-05)

Backend Modules (~47 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⚠️
/ota-listingsOTA listings (source list CRUD)
/ota-propertiesOTA connections + Lark listings (KPI cards, inline-edit)
/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 (4-Axis)

  • BookingStatusDef — configurable statuses with color, icon, sort, department (cs/payment/source/accounting)
  • Transitions stored as JSONB array within BookingStatusDef.transitions (no separate table)
    • 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

Entity Internal Codes (v3.0.4)

All 7 major entities now carry internalCode (short, human-readable code in URLs instead of raw UUID):

  • Format: {PREFIX}-{6-8 HEX} (e.g., BK-7D21C4, MY-22846F, KS-A1B2C3D4)
  • Generation: EntityCodeService (via generateFor(entity, prefix))
  • Prefixes: Property (MY), Booking (BK), Customer (KS), SupplierApartment (SA), OtaListingDetail (OL), Supplier (SP), Department (DP)
  • Route params: Accept code OR UUID (EntityCodeService resolves intelligently: UUID passthrough [0 queries], code lookup [1 indexed query per company scope])
  • URL display: Always show internalCode in headers, links, breadcrumbs; never raw UUID
  • Request bodies: Always carry real UUID from fetched entity (never the route param)
  • See: code-standards.md § 4 — Entity URLs & Internal Codes

Advanced Filters DSL (v3.0.3)

Generic server-side filtering framework with 1-level relation whitelists, hard limits, and saved views:

  • FilterCondition shape: { field: string, op: FilterOp, value?: string | number | boolean | string[] }
  • Operators: string (contains, notContains, is, isNot, isEmpty, isNotEmpty), number (eq, neq, gt, gte, lt, lte), date (isBefore, isAfter), enum/multi-select (isAnyOf, isNoneOf)
  • Transport: ?af= query param = URL-encoded JSON array
  • AND semantics: All conditions combined with AND (no OR groups)
  • Per-module registry: Field whitelist + Prisma path (hides internals); public metadata via GET /{module}/filter-fields
  • Hard limits: MAX 20 conditions, MAX 50 array values per condition (enforced on parse, rejects 400 if exceeded)
  • Saved views: FilterView model with isShared flag (company-scoped read, owner-only write)
  • Integrated modules: supplier-apartments, bookings (others can add per extension pattern)
  • See: system-architecture.md § 2f for integration flow

5. Database (30 Models, 9 Enums)

Core Entities (6)

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
ActivityLogactivity_logsHTTP request logging

Property Management (4)

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 (4)

ModelTableKey Purpose
OtaAccountota_accountsEncrypted OTA credentials + session
OtaConnectionota_connectionsProperty ↔ OTA account link
OtaRoomMappingota_room_mappingsInternal room ↔ OTA room/rate plan
SyncJobsync_jobsAsync job tracking (BullMQ)

Bookings & CRM (5)

ModelTableKey Purpose
BookingbookingsReservations from OTAs (4-axis status tracking)
BookingStatusDefbooking_status_defConfigurable statuses with transitions JSONB
AvailabilityavailabilityDaily room availability per room type
CustomercustomersGuest profiles with booking consolidation
OtaStatusDefota_status_defOTA-specific status display mappings

Rates (7)

ModelTableKey Purpose
RateratesDaily rate per room type per OTA
RateRulerate_rulesMarkup/discount/seasonal rules
RatePlanrate_plansNamed rate plan configurations
RatePlanAdjustmentrate_plan_adjustmentsAdjustments within rate plans
OtaFormulaTemplateota_formula_templatesOTA formula templates
OtaRateConfigota_rate_configsPer-OTA rate configurations
OtaRateLineota_rate_linesRate line items per OTA

Operations (4)

ModelTableKey Purpose
AlertalertsOverbooking & sync failure notifications
AuditLogaudit_logsEntity change tracking
SettingssettingsSingleton global configuration
PropertyFormulaConfigproperty_formula_configsPer-property formula overrides

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 (as of 2026-06-05)

FeatureNotes
Real OTA adapters (Booking.com, Agoda, etc.)All 4 adapters are stubs — no HTTP/scraping (Trip.com OpenTravel inbound push now live; other OTA pull adapters still stubs)
Property discoveryfetchProperties() returns empty
Cancellation syncTrip.com cancel as Type=2 stub (awaiting OTA spec); no cancel for other OTAs
Rate parity checkerNot built
Revenue analyticsNot built
LINE NotifyStub only
OTA pull-based webhooksPlanned for Booking.com, Agoda, etc. (Trip.com OpenTravel inbound push is live)
Mobile appPlanned — React Native companion

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 — Internal Documentation