Codebase Summary
Project: PTX Channel Manager (ptx-cm)
Version: 3.0.5
Last Updated: 2026-06-05
Advanced Filters: ✅ Implemented v3.0.1
List Pages Optimization: ✅ Implemented v3.0.2 (shared use-paginated-list hook, stats/facets endpoints, server-side sort)
OTA Inbound Reservations: ✅ Implemented v3.0.5 (Trip.com OpenTravel push endpoint)
1. Monorepo Dependency Graph
2. Directory Structure & Metrics
| Directory | Purpose | Est. LOC | Files |
|---|---|---|---|
| apps/api | NestJS Backend | ~34,700 | 399 TS |
| apps/web | Next.js Frontend | ~64,500 | 409 TSX/TS |
| packages/database | Prisma + migrations + seeds/imports | ~6,200 | 19 (excl. generated) |
| packages/types | Shared types/enums | ~1,100 | 7 |
| packages/config | Shared tsconfig presets | — | 3 JSON |
| docs | Documentation | ~16,000 | 29 MD |
| Total | All source (excl. generated) | ~106,500 | 830+ |
3. Backend Module Map
42 Feature Modules (apps/api/src/modules)
Auth & Users:
auth— JWT, login, refresh, password reset, rate limitingusers— User CRUD, account settings (locale, country, dateFormat)roles— Role definitions, permission bitmasks (15 modules × 4 actions)activity-logs— HTTP request logging, audit trail via middleware
Inventory & Operations:
properties— Property CRUD, timezone/currency, city/buildingName, manager assignmentroom-types— Room inventory, base rates per propertyroom-mappings— OTA ↔ local room type mappingsuppliers— Supplier/room-owner managementsupplier-room-allocations— M:N supplier ↔ room allocationsupplier-apartments— Supplier apartment hierarchy (Lark-sourced, MANUAL unmapped support)
OTA Integration:
ota-accounts— Encrypted credentials (AES-256-GCM)ota-connections— Property ↔ OTA account links + 25 Lark metadata columns; (otaAccountId, otaPropertyId) unique key; propertyId nullable for unmapped listingsota-listings— Source listing inventory CRUD (pricing, policies, specs imported from Lark); soft-delete (isActive); system is source of truth after importota-adapters— Factory + 5 adapters (Booking, Trip, Expedia, Agoda Hotel, Agoda Homes)ota-rate-configs— Per-OTA rate formula configuration + formula engineota-status— OTA-specific status definitions (per-OTA status mapping)ota-inbound— Trip.com inbound reservation push endpoint (OTA_HotelResRQ/Modify/Cancel XML dispatch), PAN redaction, durable logging, error mapping
Bookings & CRM:
bookings— Booking CRUD, upsertFromOta dedup, manual creation, 4-axis status, health computationbooking-status— Configurable BookingStatusDef definitions with department tracks (cs, payment, source, accounting), health weights, hard-deletecustomers— Guest profiles, booking consolidation, link/unlink/merge operationsrisks— Booking risk flagging, categorization (refund_heavy, customer_cancel, many_incidents), decision tracking (MANAGER+)
Availability & Rates:
availability— Calendar-based availability matrix, block/unblock date ranges, multi-room selectionrates— Base rate management per room typerate-rules— Markup/discount/seasonal rule definitionsrate-plans— Rate plan configurations with adjustmentsbulk-rates— Batch rate updates across properties/room types
Sync Engine:
sync-engine— Polling scheduler, job tracking, OTA polling/availability sync processorssync-jobs— Async job tracking with status
Chat & Threading:
chat— Booking-linked conversations, message CRUD, composingthreads— Ops triage + lead/manager tier actions (override assignee, backup-tag, rescue-thread for cancelled bookings)
Support & System:
alerts— Overbooking detection & notifications (6 alert types)dashboard— KPI metrics (occupancy, revenue, alerts, sync status)settings— App config (sync intervals, notification toggles, health flag rules)notifications— In-app notification inbox (bell icon, dropdown, full page, filters); actor attribution; user preferences (mute by type)in-app-notification— Service:createForUsers,list,markRead,markAllRead; preference-aware fan-out; daily retention job (90-day prune)notification-preferences— Per-user mute preferences;GET/PUT /api/v1/notifications/preferencesendpointscountries— Reference data for filteringfilter-views— Saved advanced filter views (CRUD). Owner-only mutate (403/404), company-scoped shared visibility (isShared → read-only). Filters revalidated server-side.health— Liveness probesprisma— Prisma client provider module
Advanced Filtering Infrastructure (apps/api/src/common/filtering)
DSL & Registry:
filter-condition.types.ts— FilterCondition DSL (?af=URL-encoded JSON). Max 20 conditions, 50 array values per condition. Operators: string (contains, is, isEmpty), number (eq, gt, gte, lt, lte), date (isBefore, isAfter, is), enum/entity (isAnyOf, isNoneOf). Day boundaries for timestamptz fields = Asia/Ho_Chi_Minh fixed (+07:00); plain-date fields (@db.Date) compare as dates only.filter-operators.ts— Per-type operator map, validation.filter-field-registry.types.ts— Module field registry format (whitelist + prismaPath, 1-level relations). Public field metadata viatoPublicFilterFields()(no prismaPath leak).module-filter-registries.ts— Global map of module → registry (NOT exported via barrel — runtime cycle avoidance). Entries: supplier-apartments, bookings, + future modules.build-prisma-where.ts— Convert FilterCondition[] → Prismawhereobject; AND-merge semantics (never clobbers country scope).parse-af-param.ts— Decode & validate?af=param, enforce hard limits (20 conditions, 50 array values).
Integration Points:
applyAdvancedFilters(where, af, module)— Called in all list services after building base where (country scope, legacy filters).GET /{module}/filter-fields— Public metadata endpoint; gated by module permissions.ListXxxDto.af?: string— Optional advanced filters param on all list DTOs.
Common Patterns (apps/api/src/common)
Entity Code Resolution:
entity-code/— @Global module.EntityCodeService.resolve(entity, idOrCode, scope?)converts route params (internal code OR UUID) to real UUID for DB queries.generateFor(entity, prefix)creates unique 6-hex codes (with 8-hex fallback on collision). Wired into properties, customers, bookings, supplier-apartments detail routes + suppliers (by supplierCode) and departments (company-scoped).
Guards (applied globally, opt-out via @Public):
JwtAuthGuard— JWT signature & expiry validationPermissionsGuard— Module:action bitmask verificationCountryScopeGuard— Country filter injection from user contextThrottlerGuard— Rate limiting (5/min login, 10/min refresh)
Decorators:
@Public()— Skip JwtAuthGuard@RequirePermission(module, action)— Bitwise permission check@CountryScope()— Inject countryScope from user.country
4. Frontend Route Map
| Route | Layer | Auth | Purpose |
|---|---|---|---|
/login, /forgot-password, /reset-password, /change-password | (auth) | Public | Auth flows |
/dashboard | (dashboard) | Required | KPI overview |
/bookings, /bookings/[id], /bookings/new | (dashboard) | Required | Booking list, detail & manual creation |
/properties, /properties/[id] | (dashboard) | Required | Property list & detail |
/customers, /customers/[id] | (dashboard) | Required | Guest CRM profiles |
/ota-accounts, /ota-accounts/connect | (dashboard) | Required | OTA account management |
/ota-properties | (dashboard) | Required | Lark OTA listings data grid with KPI, filters, inline-edit |
/ota-listings, /ota-listings/[id], /ota-listings/new | (dashboard) | Required | Source listing CRUD (pricing, policies, specs) |
/availability | (dashboard) | Required | Availability calendar & blocking |
/rates, /rates/base-rates | (dashboard) | Required | Rate management (OTA formulas) |
/rates/bulk-ops, /rates/bulk-apply | (dashboard) | Required | Bulk rate operations |
/workflows | (dashboard) | Required | Workflow hub (status CRUD, health flags) |
/workflows/designer | (dashboard) | Required | Visual workflow diagram |
/workflows/instances, /workflows/process-types | (dashboard) | Required | Process tracking |
/workflows/trigger-rules, /workflows/visibility | (dashboard) | Required | Lifecycle rules & UI visibility matrix |
/process-instances | (dashboard) | Required | BPM workflow instances |
/suppliers, /suppliers/[id] | (dashboard) | Required | Supplier management |
/alerts, /sync-jobs, /logs | (dashboard) | Required | Monitoring & audit |
/master-data, /settings, /profile | (dashboard) | Required | Configuration |
Context Provider Chain
AuthProvider → CountryProvider → ReferenceDataProvider → ThemeProvider → I18nProvider → ActivityTrackerProviderKey Components (by directory)
availability/— Availability grid, calendar picker, block-range popover, multi-room drag selectionrates/— Formula overview matrix, bulk rate operations, bulk apply formula, price calendarcustomers/— Customer table, detail view, booking history mergebookings/— Booking detail (health badge, department status bar, 4-axis status track panel, cost/margin), history timeline, status transition dialogproperties/— Property detail tabs, OTA connections, grouped sidebar list, pricing matrixworkflows/— Health flags manager, transition editor panelsettings/— Workflow status CRUD, visual diagram, transition table, UI visibilitychat/— Chat conversation list, message thread, composer, WebSocket hooks, floating chat launcher (FAB + drawer)layout/— Sidebar navigation (supports collapsibleNavGroupwith inline submenu + localStorage-persisted expand state; Organization group has Overview/Companies/Users/Departments children), topbar, country switcher, language switcherui/— Reusable primitives: data-table, dialog, tabs, status-badge, icon-picker, drawer (Radix Dialog-based right-anchored panel), etc.filter-builder/— Advanced filter UI: FilterBuilder button+panel, condition rows (type-switched operators & value inputs), field picker dropdown, entity multi-select, saved views picker. Hooks:useAdvancedFilters(encode/decode af param),useFilterFields(fetch field registry),useFilterViews(CRUD saved views). Wired into supplier-apartments & bookings pages. i18n:filters.*namespace (en, vi).
Route Builders (lib/routes.ts)
- Typed builders for entity detail URLs:
routes.property({internalCode}),routes.customer({internalCode}),routes.booking({internalCode}),routes.supplierApartment({internalCode}),routes.supplier({supplierCode}),routes.department({code}). - Enforce compile-time safety (tsc prevents bare strings or UUID interpolation).
- Old UUID URLs keep working via backend route-param resolution; new canonical URLs use internal codes.
5. Database Schema (32 Models, 9 Enums)
Auth/Users (6): users, roles, refresh_tokens, password_reset_tokens, countries, activity_logs
Inventory (4): properties, room_types, suppliers, supplier_room_allocations
OTA Integration (5): ota_accounts, ota_connections, ota_listing_details, ota_room_mappings, sync_jobs
Bookings & CRM (5): bookings, booking_status_def, availability, customers, ota_status_def
Rates (7): rates, rate_rules, rate_plans, rate_plan_adjustments, ota_formula_templates, ota_rate_configs, ota_rate_lines
Operations & Filtering (5): alerts, audit_logs, settings, property_formula_configs, filter_views
Advanced Filtering (1):
filter_views— Saved filter configurations. Fields: module (supplier-apartments|bookings|...), name, filters (JSON: FilterCondition[]), isShared (company-scoped read-only visibility), createdBy (User), company (Company). Index on (companyId, module).
Key Relationships:
- Property → [RoomTypes, OtaConnections, Availability, Manager(User)]
- OtaAccount → OtaConnections → Bookings
- RoomType → [Rates, RateRules, OtaRateConfigs, SupplierRoomAllocations]
- Booking → BookingStatusDef (4-axis: status, paymentStatus, sourceStatus, accountingStatus)
- OtaFormulaTemplate → OtaRateConfig → OtaRateLines (rate formulas)
6. Tech Stack
| Layer | Stack |
|---|---|
| Frontend | Next.js 16, React 18, Tailwind CSS, TanStack Table, SWR, react-hook-form, zod |
| Backend | NestJS 10, Passport JWT, class-validator, BullMQ, Redis |
| Database | PostgreSQL 16, Prisma 7 ORM |
| Auth | JWT + refresh tokens, HttpOnly cookies, bcrypt |
| Encryption | AES-256-GCM (OTA credentials) |
| Build | Turborepo, pnpm workspaces, TypeScript 5.7 |
| Linting & Formatting | Biome 2.4.16 (single quote, lineWidth 100) |
| Testing | Jest, @nestjs/testing |
7. Key Design Patterns
Authentication: JWT payload (sub, email, roleId, permissions, country, locale) extracted from HttpOnly access_token cookie with Bearer fallback.
Authorization: Bitmask permissions per module (VIEW=1, CREATE=2, EDIT=4, DELETE=8), 6 role presets (super_admin, admin, manager, ota, cs, fin).
OTA Adapters: Factory pattern returning strategy-based adapters for Booking, Trip.com, Expedia, Agoda (Hotel), and Agoda (Homes).
Sync Pipeline: BullMQ repeating jobs (150s) → BookingPull → AvailabilityCalc → OTA push, with SyncJob tracking for audit/retry.
Rate Engine: OTA-formula-based rate calculation. OtaFormulaTemplates define per-OTA price derivation logic. OtaRateConfigs link templates to properties. OtaRateLines store computed daily rates.
4-Axis Booking Workflow: Each booking tracks 4 department statuses (CS, Payment, Source, Accounting) via BookingStatusDef. Status transitions are role-gated with JSON-based transition rules. Health weights (ok/warning/risk) and flag rules drive bookingHealth computation.
Data Fetching: SWR for frontend caching, Context providers for global state, TanStack Table for CRUD tables.
List Page Pagination: Shared use-paginated-list hook (frontend) + server-side GET /{entity}/stats, GET /{entity}/facets, and sort whitelist (backend). All list pages (ota-properties, properties, suppliers, customers, supplier-apartments) unified onto this stack. Replaces 2–3.5MB fetch-alls with ~50KB paginated responses; preserves previous page during refresh via keepPreviousData.
8. Critical Files
| File | Purpose |
|---|---|
packages/database/prisma/schema.prisma | Source of truth for DB schema |
packages/types/src/enums.ts | OTA types, role enums, status constants |
docs/API_SPEC.md | Backend endpoint contracts |
docs/DB_DESIGN.md | Database design rationale & ER |
docs/BPM_SPEC.md | ⚠️ HISTORICAL — BPM architecture abandoned (E-21/E-22 removed 2026-04-19) |
9. Sync Engine Flow (Core Feature)
PollingScheduler (startup) → Creates BullMQ repeating jobs every 150s
↓
OtaPollingProcessor (concurrency: 3) → BookingPullService.pullBookings()
↓
OTA Adapters (factory) → fetchBookings() from each OTA
↓
BookingsService.upsertFromOta() → Dedup & upsert bookings
↓
BookingHooks → Post-upsert side effects (notifications, status checks)
↓
AvailabilitySyncProcessor (concurrency: 2) → AvailabilityCalcService.recalculate()
↓
Count bookings per date, detect isOverbooked
↓
AlertsService (if overbooking) + OTA Adapters.pushAvailability() (always)Last Updated: 2026-06-05 | Status: Active