Skip to content

System Architecture

Project: PTX Channel Manager (ptx-cm)
Version: 3.0.5
Last Updated: 2026-06-05

v3.0.5 (June 2026): Trip.com inbound OTA push endpoint (POST /api/v1/ota/trip/reservations — single route, dispatch by XML root element OTA_HotelResRQ/OTA_HotelResModifyRQ/OTA_CancelRQ → booking create/update/cancel), PAN redaction, durable OtaInboundMessage log, XML-always responses (never JSON), error mapping (OTA codes 12/450 → "Unable to process"), soft delete fix for OtaListingDetail (isActive flag per system source-of-truth rule). OTA session handlers 4→5 (added TripSessionHandler). Biome 2.4.16 replaces ESLint (single quote, lineWidth 100). Module count clarified: ~46 feature modules; Backend model count: ~50 (vs. outdated "30" claims).

v3.0.4 (June 2026): Entity internal code module — 7 entities support human-readable codes (BK-XXXXXX, CU-XXXXXX, LD-XXXXXX, etc.); EntityCodeService resolves code-or-UUID route params (UUID passthrough, code lookup → 404 if missing); lib/routes.ts typed builders enforce compile-time safety (no hardcoded strings); frontend displays internalCode in headers/breadcrumbs (never UUID); POST/PATCH bodies always send real UUID (never route param). Frontend list page optimization v3.0.2 → v3.0.3 (stats/facets ~300B vs. fetch-all 2-3.5MB).

v3.0.3 (June 2026): List pages optimization — shared use-paginated-list hook (frontend URL-synced page/sort/search, keepPreviousData flicker prevention), stats/facets endpoints (list-page optimization pattern), server-side sort whitelist (prevents arbitrary _count queries). All list pages use unified pattern (properties, suppliers, customers, ota-properties, bookings, supplier-apartments).

v3.0.2 (June 2026): OTA Listings table + source list CRUD — soft-delete, system source-of-truth rule (after Lark import, re-imports insert-only, preserve user edits), internalCode (LD- prefix), pricing/policies/specs from Lark Source List table.

v3.0.1 (June 2026): Lark-style advanced filters — generic DSL (FilterCondition[] via ?af= param), per-module field registry, FilterView saved views (owner-only mutate, company-scoped sharing), FilterBuilder UI (React hooks + components), pilot modules (supplier-apartments, bookings). AND-only semantics, 1-level relations, server-side validation + registry drift prevention. Date semantics fixed to Asia/Ho_Chi_Minh TZ.

v3.0.0 (May 2026): Lead/Manager permission layer + Threads consolidation. Mid-tier admin (LEAD+) can override assignee, backup-tag participants, flag booking risks (MANAGER+ sets finalDecision). BookingRisk model with category + note. Conversation self-relation for rescue-thread linkage (CS LEAD+ on cancelled bookings). (Risk flags + rescue threads removed May 2026 — booking_risk dropped; see §2d.) Backend: /threads/* endpoints consolidate former /chat/war-room/* routes; env var NEXT_PUBLIC_WAR_ROOM_ENABLEDNEXT_PUBLIC_THREADS_ENABLED. Frontend: /threads replaces /war-room; /threads/managed (fn-scoped) + /threads/risk (OPS-only); tier-based nav via useDeptTier hook. 47 new unit tests. All tier logic via CompanyTier.code (no new booleans on DepartmentMember). v2.9.0–v2.9.6 baseline: Chat WS fanout via Redis pub/sub (stateless), Teams-style threaded replies, War Room ops triage dashboard (3-pane list/thread/info, 4 filter chips, 131 tests).


Overview

PTX Channel Manager is a unified OTA channel management platform that auto-syncs room availability across Booking.com, Trip.com, Expedia, and Agoda (Hotel/Homes) to prevent overbookings. The system uses a monorepo structure with a Next.js frontend, NestJS backend, PostgreSQL database, and Redis queue system.


1. High-Level Architecture Diagram


1b. In-App Notification Flow


2. Sync Engine Flow (Polling → Booking → Availability)


2b. WebSocket Realtime Flow (Chat v1.2 — Stateless Pods)

Architecture (v1.2): Chat WS fanout via Redis pub/sub backplane. API pods are stateless; all subscriptions stored in Redis with TTL. Supports ≥2 replicas.

Stateless: Redis pub/sub allows any pod to route to any client. Presence/subscriptions volatile (Redis TTL). Pod restart → auto-reconnect; offline broadcasts lost (acceptable for WS).


2c. OTA Inbound XML Push Pipeline (Trip.com OpenTravel)

Architecture (v3.0.5): Trip.com CM Notify model sends reservation notifications as XML (root element OTA_HotelResRQ / OTA_HotelResModifyRQ / OTA_CancelRQ). System accepts all message types at a single endpoint POST /api/v1/ota/trip/reservations, validates POS credentials, dispatches to handlers by root element, returns XML response (never JSON).

Handlers: root OTA_HotelResRQ → TripNewReservationHandler (create), OTA_HotelResModifyRQ → TripModifyReservationHandler (update), OTA_CancelRQ → TripCancelReservationHandler (cancel) — all dispatched from the single /ota/trip/reservations route.

OtaInboundMessage (audit log): otaType, messageType (res_new|modify|cancel), echoToken, otaBookingId, hotelCode, rawXml (PAN-redacted), outcome, errorCode, processingMs, remoteIp, createdAt.

Error Codes: POS auth fail / property not found → 12. Business rule violation → 450. All errors return HTTP 200 + XML RS (OTA requirement, never 4xx).

Key Features: PAN redaction before persist, immutable audit trail, constant-time POS comparison, XML-always responses, extensible handler registry.


2d. Threads Module: Lead/Manager Booking-Thread Inbox

Purpose: Unified console for booking-linked chat threads, gated by department tier (LEAD+) for inline assignment override and backup-tagging.

History: v3.0.0 also shipped risk flagging (BookingRisk model, /threads/risk, /risks/:bookingId) and rescue threads (POST /threads/rescue). Both were removed in May 2026 (booking_risk table dropped); the console is now a pure thread inbox.

Routes (verified apps/api/src/modules/threads/threads.controller.ts):

  • GET /api/v1/threads/list — Paginated booking-thread feed (live-activity sort). Country-scoped.
  • GET /api/v1/threads/unread-count — Unread count for sidebar badge.
  • GET /api/v1/threads/managed?fn={function} — LEAD+ per-department view, filtered by the booking stage active for that function (operations = no stage filter, sees all).
  • PATCH /api/v1/threads/:id/assignee — Override assignee (LEAD+ → Participant.ADMIN). Idempotent.
  • POST /api/v1/threads/:id/participants/backup — Backup-tag user (LEAD+, idempotent MEMBER add).
  • GET /api/v1/users/me/dept-tiers{ [DepartmentFunction]: TierCode } for frontend visibility (useDeptTier).

Frontend Routes:

  • /threads — 3-pane inbox (gated by NEXT_PUBLIC_THREADS_ENABLED): thread list (searchable, filterable) | conversation (center) | booking context pane (right). Scoped styles in apps/web/app/threads.css.
  • /threads/managed — Per-function scoped view (LEAD+ only), components under apps/web/components/threads-managed/.

Authorization:

  • Tier ladder: STAFF < LEAD < MANAGER < DIRECTOR (DIRECTOR inherits MANAGER privileges)
  • LEAD+: override assignee, backup-tag
  • Identity: DepartmentMember.companyTier.code (single source of truth; no booleans)

Audit:

  • All privileged writes logged: Conversation, Participant in AUDITABLE_MODELS

Key Components (apps/web/components/threads/):

  • Page & list: thread-page.tsx, thread-list.tsx, thread-list-row.tsx, thread-list-filters.tsx, thread-list-search.tsx
  • Conversation: thread-conversation.tsx (+ header/body/composer variants)
  • Context pane: thread-context-pane.tsx (+ status strip, activity log, sections)
  • Hooks: use-thread-list.ts, use-thread-url-state.ts, use-thread-list-invalidator.ts

2e. Floating Chat UI (Dashboard Integration)

Architecture Overview:

  • ChatProvider hoisted to app/(dashboard)/layout.tsx — single context instance per tab, available to all dashboard routes
  • FloatingChatLauncher renders persistent FAB (z-30) + drawer (z-40) on authenticated pages
  • FAB hidden on /chat* routes (avoid dual UI)
  • Drawer reuses existing ConversationList and Thread components (no duplication)
  • Real-time unread count via useChatUnread() hook (WS-driven)

Component Hierarchy:

(dashboard)/layout.tsx
  ├─ ChatProvider (hoisted, single WS)
  ├─ FloatingChatLauncher
  │  ├─ FloatingChatButton (FAB, z-30)
  │  └─ Drawer (Radix Dialog, z-40)
  │     └─ FloatingChatDrawer (list → thread state machine)
  └─ Page content / nested routes

Z-Index Convention:

  • FAB: z-30 (visible, clickable, below drawer)
  • Drawer + overlay: z-40 (sits above FAB, below modals)
  • ConfirmDialog: z-50 (always on top of drawer)
  • Toaster: z-[999999999] (sonner default, never overridden)

See apps/web/components/ui/README.md for full z-index table.


2f. Advanced Filters (Lark-style DSL)

Purpose: Generic, module-scoped query language for list endpoints. Replaces per-endpoint custom filters with a unified FilterCondition DSL (AND semantics).

Architecture:

Backend (NestJS):

  1. DSL Definition (common/filtering/):

    • FilterCondition interface: { field, op, value } where op ∈
    • Transport: ?af=encodeURIComponent(JSON.stringify(FilterCondition[])) (URL query param)
    • Hard limits: max 20 conditions, max 50 array values per condition
  2. Field Registry (per-module):

    • File: {module}/module-filter-registry.ts → exports whitelist of filterable fields
    • Format: { [fieldName]: { type: 'string'|'number'|'date'|'enum', prismaPath: 'path.to.field', relatedModule?: 'other-module' } }
    • 1-level relations only (e.g., 'property.name' OK, 'property.manager.name' not supported)
    • Public metadata: GET /{module}/filter-fields (gated by module permissions) → returns field metadata WITHOUT prismaPath
  3. Filter Processing:

    • parseAfParam() — Decode & validate ?af= param; enforce hard limits
    • buildPrismaWhere() — Convert FilterCondition[] → Prisma where object (no loops, 1-level relations)
    • applyAdvancedFilters() — Merge af where into existing where (country scope never clobbered)
    • Module registries centralized in module-filter-registries.ts map (NOT exported via barrel — avoids runtime cycles)
  4. Pilot Modules (both updated for af support):

    • supplier-apartments — Registry: supplierId, propertyId, roomType, specialist (string), hasInputError (boolean), unmatchedOnly (boolean), hasNoImages (boolean)
    • bookings — Registry: propertyId, otaType (enum), status, checkIn/checkOut (date ranges: isBefore, isAfter, is for day boundaries), guestName, bookingHealth
  5. Date Semantics (fixed business TZ):

    • Timestamptz day boundaries computed as Asia/Ho_Chi_Minh (+07:00), no DST
    • Plain-date fields (@db.Date) compare as calendar dates (no TZ math)
    • Currency fields & BigInt amounts coerced transparently
  6. Saved Views (FilterView model):

    • Table: filter_views(id, module, name, filters, isShared, createdById, companyId, createdAt, updatedAt)
    • CRUD: GET/POST/PATCH/DELETE /api/v1/filter-views (owned by creator, 403/404 on unauthorized mutate)
    • Visibility: isShared: false → creator-only; isShared: true → company-scoped read (any user in company can see, but only creator can mutate)
    • Filters revalidated server-side on load/apply (prevents field-registry drift)

Frontend (React):

  • Components (apps/web/components/ui/filter-builder/):

    • FilterBuilder — Button + dropdown panel with condition rows
    • FilterConditionRow — Field picker + operator picker (type-switched) + value input (text, number, date range, or entity multi-select)
    • EntitySelect — Load-all dropdown for enums/related entities (e.g., booking status values, property list)
    • FilterViewPicker — Save/load/delete saved views dropdown
  • Hooks (filter-builder/):

    • useAdvancedFilters(module) — Encode/decode af param, manage URL state
    • useFilterFields(module) — Fetch & cache field registry from GET /{module}/filter-fields
    • useFilterViews(module) — CRUD saved views (POST create, PATCH update, DELETE remove)
  • Integration:

    • Wired into supplier-apartments list page + bookings list page
    • Button trigger: FilterBuilder icon near search bar (Tailwind primary color)
    • Value persists in ?af= query param + localStorage for drafts
    • i18n: filters.* namespace (en, vi) — labels, operators, validation msgs

Extension Path (new module):

  1. Create src/modules/{module}/module-filter-registry.ts with field whitelist
  2. Add entry to module-filter-registries.ts map
  3. Wire af?: string param into List{Module}Dto
  4. Call applyAdvancedFilters(where, af, '{module}') in service list method
  5. Add <FilterBuilder module="{module}" /> to frontend list page
  6. (Optional) Add custom field picker UI if module has special filtering logic

Bookings ↔ FilterView Coexistence Note:
Legacy localStorage-based saved views on /bookings screen remain for backward compatibility. New FilterView backend views are gradually migrated; consolidation deferred.


2g. Entity Internal Code Resolution (v3.0.4)

Purpose: Human-readable codes (BK-A1B2C3) alongside UUIDs on detail routes. Enables shareable URLs without exposing database IDs.

Supported Entities (7): Booking (BK), Customer (CU), Property (country-based), Supplier (SA), Department, OtaListingDetail (LD), SupplierApartment (SA).

Format: {PREFIX}-{6HEX} (auto-generated, collision-retry to 8 hex). EntityCodeService resolves:

  • UUID input → passthrough (free, no DB hit)
  • Code input → lookup by unique index → UUID or 404
  • Detail routes accept code OR UUID; request bodies always send UUID

Frontend Integration: lib/routes.ts typed builders prevent UUID interpolation. Example: routes.booking({internalCode: 'BK-A1B2C3'})/bookings/BK-A1B2C3. Display shows internalCode in headers/breadcrumbs (never UUID).


3. Authentication & Authorization Flow


4. Database Schema (49 Models)

Domain Groups (verified against packages/database/prisma/schema.prisma):

  • Auth & Users (5): users, roles, refresh_tokens, password_reset_tokens, activity_logs
  • Organization (5): companies, company_tiers, departments, department_roles, department_members
  • Master Data (4): countries, cities, districts, settings
  • Inventory (6): properties, room_types, suppliers, supplier_organizations, supplier_apartments, supplier_room_allocations
  • OTA (7): ota_accounts, ota_connections, ota_room_mappings, ota_listing_details, ota_inbound_messages, ota_status_def, sync_jobs
  • Bookings (5): bookings, booking_status_def, booking_escalations, availability, customers
  • Rates (8): rates, rate_rules, rate_plans, rate_plan_adjustments, ota_formula_templates, ota_rate_configs, ota_rate_lines, property_formula_configs
  • Chat & Notifications (6): conversations, participants, messages, attachments, notifications, user_notification_preferences
  • Operations (3): alerts, audit_logs, filter_views

Key Relations:

  • Property ← [RoomTypes, OtaConnections (nullable propertyId), Bookings, SupplierApartments, OtaListingDetails]
  • RoomType ← [Rates, RateRules, Availability, OtaRoomMappings, SupplierRoomAllocations, Bookings, OtaListingDetails]
  • OtaAccount ← OtaConnection (unique on otaAccountId + otaPropertyId) ← [OtaRoomMappings, SyncJobs, Rates]
  • Booking → 4 status axes via BookingStatusDef keys (status / paymentStatus / sourceStatus / accountingStatus), Customer, Supplier, Conversation (kind=BOOKING)
  • OtaListingDetail → [RoomType, Property]; ← SupplierApartments (Lark source data post-import)
  • OtaInboundMessage: standalone durable XML log (no FK; soft refs via hotelCode/echoToken)

5. Monorepo Structure

ptx-cm/
├── apps/
│   ├── api/                          # NestJS 10 Backend
│   │   ├── src/
│   │   │   ├── modules/              # 46 feature modules
│   │   │   ├── common/               # Guards, filters, decorators
│   │   │   ├── utils/                # Helpers, crypto, pagination
│   │   │   └── main.ts               # Bootstrap
│   │   └── package.json
│   │
│   └── web/                          # Next.js 16 Frontend
│       ├── app/                      # App Router
│       │   ├── (auth)/               # Public routes
│       │   ├── (dashboard)/          # Protected routes
│       │   └── api/[...path]/        # Backend proxy
│       ├── components/               # React components (27 domain dirs)
│       ├── hooks/                    # Custom hooks
│       ├── lib/                      # Utilities + context providers
│       └── package.json

├── packages/
│   ├── database/                     # Prisma
│   │   ├── prisma/                   # Schema + migrations
│   │   ├── src/
│   │   │   └── client.ts
│   │   └── package.json
│   │
│   ├── types/                        # Shared TypeScript
│   │   ├── src/
│   │   │   ├── api.types.ts          # Request/response DTOs
│   │   │   └── enums.ts              # OtaType, AlertSeverity, etc.
│   │   └── package.json
│   │
│   └── config/                       # Shared tsconfig presets
│       ├── tsconfig/                 # base.json, nestjs.json, nextjs.json
│       └── package.json

├── docs/                             # Documentation
│   ├── system-architecture.md        # This file
│   ├── codebase-summary.md           # File counts & module map
│   ├── code-standards.md             # Naming, patterns, error handling
│   ├── project-overview-pdr.md       # Requirements & PDR
│   ├── development-roadmap.md        # Feature status & milestones
│   ├── SRD.md                        # System Requirements
│   ├── API_SPEC.md                   # REST API spec
│   ├── DB_DESIGN.md                  # Database design
│   └── UI_SPEC.md                    # Design system & screens

├── docker-compose.yml                # PostgreSQL 16 + Redis 7
├── turbo.json                        # Turborepo config
├── pnpm-workspace.yaml               # pnpm workspaces
└── package.json                      # Root config

6. Backend Module Architecture

Core Modules (46 feature modules total)

ModulePurposeKey Files
authJWT authentication, login, refresh, password resetauth.service.ts, jwt.strategy.ts
usersUser CRUD, account settingsusers.service.ts, users.controller.ts
rolesRole definitions, permission bitmasksroles.service.ts
activity-logsHTTP request logging, audit trailactivity-log.middleware.ts
notificationsEmail service (Resend API + Mailpit in dev)notifications.service.ts
in-app-notificationsPer-user notification inbox, retention jobin-app-notifications.service.ts, in-app-notifications.controller.ts
propertiesProperty CRUD, timezone/currency assignmentproperties.service.ts
room-typesRoom inventory, base ratesroom-types.service.ts
room-mappingsOTA ↔ local room type mappingroom-mappings.service.ts
suppliersSupplier/room-owner managementsuppliers.service.ts
supplier-room-allocationsM:N supplier ↔ room allocationsupplier-room-allocations.service.ts
ota-accountsEncrypted OTA credentials (AES-256-GCM)ota-accounts.service.ts
ota-connectionsProperty ↔ OTA account linksota-connections.service.ts
ota-adaptersFactory + 5 adapters (Booking, Trip, Expedia, Agoda Hotels, Agoda Homes)ota-adapter.factory.ts
ota-rate-configsPer-OTA rate formula configurationota-rate-configs.service.ts
ota-statusOTA-specific status definitionsota-status.service.ts
ota-listingsOTA source listing inventory CRUD (pricing, policies, specs); soft-delete (isActive)ota-listings.service.ts
ota-inboundTrip.com inbound OTA XML push (HotelResRQ/Modify/Cancel), PAN redaction, durable loggingota-inbound.service.ts
bookingsBooking CRUD, upsertFromOta dedupbookings.service.ts
booking-statusConfigurable status definitionsbooking-status.service.ts
booking-status-transitionStatus state machine transitionsbooking-status-transition.service.ts
booking-hooksPost-status-change side effectsbooking-hooks.service.ts
customersGuest CRM, booking consolidation, link/unlink/mergecustomers.service.ts
availabilityCalendar matrix, block/unblock date rangesavailability.service.ts
ratesBase rate management per room typerates.service.ts
rate-rulesMarkup/discount/seasonal rulesrate-rules.service.ts
rate-plansRate plan configurations with adjustmentsrate-plans.service.ts
bulk-ratesBatch rate updates across propertiesbulk-rates.service.ts
process-typesBPM workflow type definitionsprocess-types.service.ts
process-instancesActive workflow instance trackingprocess-instances.service.ts
process-statusProcess status definitionsprocess-status.service.ts
process-transitionsState machine transition rulesprocess-transitions.service.ts
sync-engineOrchestration of polling, pulling, syncingsync-engine.service.ts
sync-jobsJob tracking with statussync-jobs.service.ts
alertsOverbooking detection, notificationsalerts.service.ts
chatInternal messaging DM/channels/booking-contextchat.controller.ts, messages.service.ts
chat.realtimeWebSocket gateway, typing, presence, heartbeatchat.gateway.ts, rooms.service.ts, presence.service.ts, typing.service.ts, heartbeat.service.ts
threadsBooking-linked chat threads, lead/manager tier ops (v3.0.0)threads.controller.ts, threads.service.ts, lead-helpers.ts
risksBooking risk flagging, categorization, decision tracking (v3.0.0)risks.controller.ts, risks.service.ts
dashboardAggregated KPI metricsdashboard.service.ts
settingsApp config (booking_pull_minutes, etc)settings.service.ts
healthLiveness probeshealth.controller.ts
countriesReference data for country filteringcountries.service.ts
prismaPrisma client providerprisma.service.ts

Guard & Decorator Pattern

typescript
// Global guards (applied in order):
1. JwtAuthGuard          // Validates JWT signature & expiry
2. PermissionsGuard      // Checks module:action bitmask
3. CountryScopeGuard     // Applies country filter to query

// Decorators to opt out or customize:
@Public()                // Skip JwtAuthGuard
@RequirePermission(MODULE.PROPERTIES, ACTIONS.CREATE)
@CountryScope()          // Injects countryScope param

7. Frontend Route Architecture

Route PatternLayerAuthentication
/login, /forgot-password, /reset-password, /change-password(auth)Public
/dashboard(dashboard)Protected + JWT
/bookings, /bookings/[id], /bookings/new(dashboard)Protected + JWT
/properties, /properties/[id](dashboard)Protected + JWT
/customers, /customers/[id](dashboard)Protected + JWT
/availability(dashboard)Protected + JWT
/rates, /rates/base-rates, /rates/bulk-ops, /rates/bulk-apply(dashboard)Protected + JWT
/ota-accounts, /ota-accounts/connect(dashboard)Protected + JWT
/workflows, /workflows/designer, /workflows/instances(dashboard)Protected + JWT
/workflows/process-types, /workflows/trigger-rules, /workflows/visibility(dashboard)Protected + JWT
/process-instances(dashboard)Protected + JWT
/suppliers, /suppliers/[id](dashboard)Protected + JWT
/alerts, /sync-jobs, /logs(dashboard)Protected + JWT
/master-data, /settings, /profile(dashboard)Protected + JWT

Context Provider Chain

AuthProvider (JWT + user state)

CountryProvider (VN/ID/MY filter)

ReferenceDataProvider (Countries cache)

ThemeProvider (Light/dark mode)

I18nProvider (en/vi locale)

ActivityTrackerProvider (Session timeout)

8. Tech Stack Summary

LayerTechnologyVersion
FrontendNext.js + React + Tailwind CSS16 + 18
Frontend StateSWR + Context API-
Frontend Formreact-hook-form + zod-
Frontend TablesTanStack Table (react-table)-
BackendNestJS + Passport10
Backend Validationclass-validator-
Backend QueueBullMQ + Redis-
DatabasePostgreSQL + Prisma ORM16 + 7
AuthenticationJWT + bcrypt-
EncryptionAES-256-GCM-
Build ToolTurborepo + pnpm-
LanguageTypeScript5.7
Linting & FormattingBiome (replaced ESLint)2.4.16
TestingJest + @nestjs/testing-

9. Key Design Patterns

Authentication Pattern

  • JWT payload includes: sub, email, roleId, permissions (PermissionMap), country, locale
  • Token extraction: HttpOnly cookie access_token → fallback to Bearer header
  • Guards applied per controller method or globally

Authorization Pattern

  • Bitmask-based permissions per module (VIEW=1, CREATE=2, EDIT=4, DELETE=8)
  • Role-based presets: super_admin, admin, manager, ota, cs, fin
  • Country-scoped queries: all user queries filtered by countryScope middleware

OTA Adapter Pattern

  • Factory pattern: OtaAdapterFactory.create(otaType) returns adapter instance
  • Strategy interface: IOtaAdapter { fetchBookings(), pushAvailability() }
  • 5 implementations: BookingAdapter, TripAdapter, ExpediaAdapter, AgodaHotelAdapter, AgodaHomesAdapter

Sync Pipeline Pattern

  • Repeating jobs via BullMQ scheduler (150s interval)
  • Multi-stage processing: polling → booking pull → availability calc → OTA push
  • Job tracking: SyncJob records created for audit + retry logic

Lark Import Pipeline (v3.0.1)

OTA Properties/Connections:

  • Source: Lark Bitable table "2.3 Property List" → CSV export via PTX_SOP_LARK project (export_lark_property_list.py)
  • Lookup: Lark select option IDs resolved via lark_base_structure.json → human labels (e.g. "2. Active to Sale" for OTA status)
  • Import: pnpm --filter @ptx-cm/database import:ota-properties runs packages/database/prisma/import_lark_ota_properties.ts
  • Idempotency: Upsert keyed on (otaAccountId, otaPropertyId); auto-creates placeholder OtaAccount per platform (label "X (Lark)", status expired)
  • Matching: Building-name → property via cleanName logic (same as supplier apartments); unmapped listings stored with NULL propertyId
  • Report: Created/updated/skipped/unmapped counts logged; first run: 1723 CSV rows → 1712 connections (11 skipped, 174 unmapped)
  • Helpers: Shared packages/database/prisma/lark_import_helpers.ts — cleanName, labelResolver, placeholder account creation

Source Listing Details (v3.0.2 — NEW 2026-06-05):

  • Source: Lark "Source List" table → CSV export
  • Import: pnpm --filter @ptx-cm/database import:source-listings runs packages/database/prisma/import_lark_source_list.ts
  • Data: Pricing (costWd, costWk, margins), policies (check-in, restrictions, child policy), specs (roomSize, bedType, maxPax, amenities)
  • Upsert Key: (roomTypeId, propertyId) — maps Lark source to internal room type
  • Auto-Gen: internalCode (LD-xxxxxx prefix) created via generateFor('OtaListingDetail', 'LD')
  • OPERATIONAL NOTE: After initial import, the SYSTEM database becomes source of truth. Script MUST NOT re-run in overwrite mode. Future re-imports insert NEW rows only (preserving user edits on existing rows). This protects operational data (custom margins, restriction rules, etc.).
  • Soft-Delete: Listings can be deleted (isActive=false) and restored (PATCH { isActive: true }) without re-importing

Data Fetching (Frontend)

  • SWR for data caching & revalidation
  • Context providers for global state (auth, country, reference data)
  • TanStack Table for sorting, pagination, filtering

10. Key Enums & Types

OTA Enums:

  • OtaType: booking, agoda, traveloka, expedia
  • ConnectionStatus: active, expired, error, requires_2fa
  • TwoFactorMethod: none, totp, manual
  • SyncJobType: pull_bookings, push_availability, push_rates, verify
  • SyncJobStatus: pending, running, completed, failed

Booking Enums:

  • AlertType: overbooking, sync_failure, session_expired, sla_breach, missing_deposit, missing_contact, unconfirmed_checkin, payment_mismatch
  • AlertSeverity: critical, warning, info
  • AuditAction: create, update, delete

Rate Enums:

  • RateRuleType: markup, discount, seasonal

Permission Modules (15):
DASHBOARD, PROPERTIES, ROOM_TYPES, OTA_ACCOUNTS, BOOKINGS, CUSTOMERS, AVAILABILITY, SYNC_JOBS, RATES, ALERTS, SETTINGS, USERS, SUPPLIERS, COUNTRIES, ROLES


11. Deployment Architecture

Development:

  • Monorepo dev servers: pnpm dev starts both apps + BullMQ listeners
  • PostgreSQL in Docker: port 5433
  • Redis in Docker: port 6379
  • Mailpit (email catcher): ports 1025/8025

Production:

  • Separate container images: API (port 3002), Web (port 3100)
  • PostgreSQL managed service (Cloud SQL / RDS)
  • Redis managed service (ElastiCache / Upstash)
  • Email via Resend API
  • Sync jobs persist in BullMQ (Redis)

12. Country Scope Module

Backend: apps/api/src/modules/country-scope/country-scope.config.ts, country-scope.guard.ts, country-scope.decorator.ts, country-scope.helpers.ts, index.ts
Frontend: apps/web/lib/country-scope/config.ts, country-context.tsx, country-selector.tsx, index.ts

Toggle

Default flag value: true (preserves existing behavior).

EnvEffect
COUNTRY_SCOPE_ENABLED=falseBE bypass: 19 modules unfiltered, all helpers short-circuit
NEXT_PUBLIC_COUNTRY_SCOPE_ENABLED=falseFE bypass: provider returns country: 'all', selector renders null

Strict parser: false, 0, no, empty string (case/whitespace-insensitive) → DISABLED. Anything else → ENABLED.

Re-enable Runbook

  1. Set both env vars =true
  2. Restart API process (BE config re-read on boot, idempotent boot log)
  3. Rebuild FE bundle: pnpm --filter @ptx/web build then deploy (or restart pnpm dev locally — Next.js dev reads runtime NEXT_PUBLIC_* on first render)
  4. Boot log confirms: [CountryScope] ENABLED (env: COUNTRY_SCOPE_ENABLED=true)

Known Gaps

  • WebSocket gateway (apps/api/src/modules/chat/realtime/chat.gateway.ts) does not enforce country scope (pre-existing gap, not introduced by this module). Flag controls HTTP layer only.

Schema

User.country, Supplier.country, Property.country retained NOT NULL. Admins still enter country when creating data — flag only controls runtime filtering, not data shape.


Last Updated: 2026-06-05
Status: Active — v3.0.1 with Lark OTA listings data import pipeline; v3.0.0 lead/manager permission layer, threads consolidation, risk management, tier-based nav, country-scope feature flag

PTX Channel Manager — Internal Documentation