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:
- 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/ # ESLint/TS shared configs
├── 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 (22 models) |
| 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-02-20)
Backend Modules (25 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 | ⚠️ |
| /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
BookingStatusDef— configurable statuses with color, icon, sortBookingStatusTransition— state machine transitions with: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
5. Database (22 Models)
Core Entities
| 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 |
Property Management
| 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
| 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 |
Operations
| Model | Table | Key Purpose |
|---|---|---|
| Booking | bookings | Reservations from OTAs |
| BookingStatusDef | booking_status_def | Configurable booking statuses |
| BookingStatusTransition | booking_status_transition | State machine transitions |
| Availability | availability | Daily room availability per room type |
| SyncJob | sync_jobs | Async job tracking (BullMQ) |
| Rate | rates | Daily rate per room type per OTA |
| RateRule | rate_rules | Markup/discount/seasonal rules |
| Alert | alerts | Overbooking & sync failure notifications |
| AuditLog | audit_logs | Entity change tracking |
| Settings | settings | Singleton global configuration |
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
| Feature | Notes |
|---|---|
| Real OTA adapters | All 4 adapters are stubs — no HTTP/scraping |
| Property discovery | fetchProperties() 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 sync | No cancelBooking in adapter interface |
| Rate parity checker | Not built |
| Revenue analytics | Not built |
| LINE Notify | Stub only |
| Force Sync UI button | Backend exists, no frontend button |
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