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:
- Connects OTA accounts & auto-discovers properties
- Polls OTA extranets every 2-3 min for new bookings
- Auto-pushes updated availability to all connected OTAs
- Alerts on sync failures or overbookings
- 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 mockupsTech Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 18, Tailwind CSS, TanStack Table, react-hook-form, zod, SWR |
| Backend | NestJS 10, Passport JWT, class-validator, BullMQ |
| Database | PostgreSQL 16, Prisma 7 ORM (30 models, 9 enums) |
| Queue/Cache | Redis 7, BullMQ job queues |
| Auth | JWT access (15m) + refresh (7d), HttpOnly cookies, bcryptjs |
| Encryption | AES-256-GCM (OTA credentials) |
| Build | Turborepo, 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)
| Module | Status | Purpose |
|---|---|---|
| auth | ✅ | JWT + refresh tokens, login/logout/forgot/reset password |
| dashboard | ✅ | KPI cards, sync status, recent bookings |
| properties | ✅ | CRUD for hotel properties |
| room-types | ✅ | Room inventory per property |
| ota-accounts | ⚠️ | CRUD + encrypted creds. Test/refresh are stubs |
| ota-connections | ✅ | Link properties ↔ OTA accounts |
| room-mappings | ✅ | Map internal rooms to OTA room/rate IDs |
| bookings | ✅ | List/detail/export. Status transitions via workflow |
| booking-status | ✅ | BookingStatusDef CRUD |
| booking-status-transition | ✅ | Workflow transitions CRUD |
| booking-hooks | ✅ | audit_log, update_availability, send_notification hooks |
| alerts | ✅ | Create/resolve overbooking & sync failure alerts |
| sync-jobs | ✅ | Job history, force-sync endpoint |
| sync-engine | ⚠️ | BullMQ orchestration ✅. OTA adapters are all stubs |
| ota-adapters | ⚠️ | 4 adapters defined, all return empty/false |
| settings | ✅ | Global config (sync intervals, notification, SMTP) |
| users | ✅ | CRUD + password reset (temp + email link) |
| roles | ✅ | 7 preset roles + custom, bitwise permissions |
| suppliers | ✅ | CRUD + CSV import/export |
| supplier-room-allocations | ✅ | M:N Supplier ↔ RoomType with room count |
| countries | ✅ | VN/ID/MY with pill colors, sort order |
| notifications | ✅ | Email via SMTP fallback chain. LINE Notify stub |
| activity-logs | ✅ | HTTP request logging, super admin only |
| health | ✅ | Liveness probe |
| prisma | ✅ | Prisma service provider |
Frontend Pages
| Route | Screen | Status |
|---|---|---|
| /login | Login | ✅ |
| /dashboard | Dashboard + activity log | ✅ |
| /properties | Properties list | ✅ |
| /properties/[id] | Property detail (rooms, OTA connections) | ✅ |
| /bookings | Bookings list (filters, search, export) | ✅ |
| /bookings/[id] | Booking detail (status transitions, audit) | ✅ |
| /ota-accounts | OTA accounts list | ✅ |
| /ota-accounts/[id] | OTA account detail | ⚠️ |
| /ota-listings | OTA listings (source list CRUD) | ✅ |
| /ota-properties | OTA connections + Lark listings (KPI cards, inline-edit) | ✅ |
| /suppliers | Supplier list (import/export CSV) | ✅ |
| /suppliers/[id] | Supplier detail + room allocations | ✅ |
| /alerts | Alert list + resolve | ✅ |
| /sync-jobs | Sync job log | ✅ |
| /logs | Activity/client event logs | ✅ |
| /settings | 7 tabs: Users, Roles, Booking Statuses, Workflow, Prefs, Notifications, System | ✅ |
| /profile | User profile + password change | ✅ |
Frontend Contexts & Hooks
| Context/Hook | Purpose |
|---|---|
AuthContext | User session, permissions, JWT hydration |
CountryContext | Country filter (localStorage persistence) |
ThemeContext | Dark/light mode toggle |
ReferenceDataContext | Shared reference data (countries, roles) |
ActivityTrackerProvider | Client-side activity tracking |
useApi | SWR-based data fetching hook |
usePermission | Permission 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)uiConfigper role (sections, buttons, editable fields)
Supplier Room Allocation
Supplier— owner of apartment rooms (code, name, bank details, contact)SupplierRoomAllocation— M:N junction: Supplier ↔ RoomType withroomCount- 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)
| Model | Table | Key Purpose |
|---|---|---|
| Role | roles | Named roles with JSONB permissions |
| User | users | Staff accounts with country, role FK, date format |
| RefreshToken | refresh_tokens | JWT revocation tracking |
| PasswordResetToken | password_reset_tokens | SHA-256 hashed reset tokens |
| Country | countries | VN/ID/MY with pill colors |
| ActivityLog | activity_logs | HTTP request logging |
Property Management (4)
| Model | Table | Key Purpose |
|---|---|---|
| Property | properties | Hotel properties (country, timezone, currency) |
| RoomType | room_types | Room categories per property |
| Supplier | suppliers | Room owners (code, contact, bank info) |
| SupplierRoomAllocation | supplier_room_allocations | M:N Supplier↔RoomType with room count |
OTA Integration (4)
| Model | Table | Key Purpose |
|---|---|---|
| OtaAccount | ota_accounts | Encrypted OTA credentials + session |
| OtaConnection | ota_connections | Property ↔ OTA account link |
| OtaRoomMapping | ota_room_mappings | Internal room ↔ OTA room/rate plan |
| SyncJob | sync_jobs | Async job tracking (BullMQ) |
Bookings & CRM (5)
| Model | Table | Key Purpose |
|---|---|---|
| Booking | bookings | Reservations from OTAs (4-axis status tracking) |
| BookingStatusDef | booking_status_def | Configurable statuses with transitions JSONB |
| Availability | availability | Daily room availability per room type |
| Customer | customers | Guest profiles with booking consolidation |
| OtaStatusDef | ota_status_def | OTA-specific status display mappings |
Rates (7)
| Model | Table | Key Purpose |
|---|---|---|
| Rate | rates | Daily rate per room type per OTA |
| RateRule | rate_rules | Markup/discount/seasonal rules |
| RatePlan | rate_plans | Named rate plan configurations |
| RatePlanAdjustment | rate_plan_adjustments | Adjustments within rate plans |
| OtaFormulaTemplate | ota_formula_templates | OTA formula templates |
| OtaRateConfig | ota_rate_configs | Per-OTA rate configurations |
| OtaRateLine | ota_rate_lines | Rate line items per OTA |
Operations (4)
| Model | Table | Key Purpose |
|---|---|---|
| Alert | alerts | Overbooking & sync failure notifications |
| AuditLog | audit_logs | Entity change tracking |
| Settings | settings | Singleton global configuration |
| PropertyFormulaConfig | property_formula_configs | Per-property formula overrides |
Enums
OtaType, ConnectionStatus, TwoFactorMethod, SyncJobType, SyncJobStatus, AlertType, AlertSeverity, AuditAction, RateRuleType
6. Key Patterns
Auth Flow
POST /auth/login→ JWT access (15m cookie) + refresh (7d cookie)- Frontend interceptor: 401 → refresh queue → retry. All concurrent 401s wait for single refresh.
- 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)
| Feature | Notes |
|---|---|
| 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 discovery | fetchProperties() returns empty |
| Cancellation sync | Trip.com cancel as Type=2 stub (awaiting OTA spec); no cancel for other OTAs |
| Rate parity checker | Not built |
| Revenue analytics | Not built |
| LINE Notify | Stub only |
| OTA pull-based webhooks | Planned for Booking.com, Agoda, etc. (Trip.com OpenTravel inbound push is live) |
| Mobile app | Planned — React Native companion |
8. Development Workflow
Commands
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)
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